From a2b599287a0376f238fb3af8450c3f781ddfcd6e Mon Sep 17 00:00:00 2001 From: cj-vana Date: Thu, 25 Sep 2025 08:58:55 -0600 Subject: [PATCH 1/3] feat: Add signal generator and loopback measurement to capture agent v0.2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement comprehensive signal generation capabilities: • Pink noise generator using Voss-McCartney algorithm • White noise generator for system calibration • Sine wave generator with 20Hz-20kHz frequency range • Logarithmic sine sweep for system testing • Click track generator with adjustable BPM • Configurable output routing to any audio channel - Add internal loopback measurement mode: • Route generated signals directly to reference channel • Enable measurement of DSP chains and software signal paths • Support for analyzing systems without physical connections - Update version numbers: • Capture agent: 0.1.14 → 0.2.0 (major feature release) • Web app: 1.5.6.6 → 1.5.6.7 • Update GitHub CI workflow installer versions --- .github/workflows/build-installers.yml | 8 +- CHANGELOG.md | 23 + README.md | 2 + .../capture-agent-py/capture_agent/audio.py | 2 +- agents/capture-agent-py/capture_agent/dsp.py | 8 +- .../capture-agent-py/capture_agent/schema.py | 23 +- .../capture-agent-py/capture_agent/server.py | 266 ++++++- .../capture_agent/signal_generator.py | 269 +++++++ agents/capture-agent-py/poetry.lock | 688 ++++++++++++++++++ agents/capture-agent-py/pyproject.toml | 5 +- apps/web/index.html | 2 +- apps/web/package.json | 2 +- .../src/components/analyzer/ProSettings.tsx | 42 +- .../analyzer/SignalGeneratorSettings.tsx | 373 ++++++++++ apps/web/src/pages/AnalyzerProPage.tsx | 42 +- packages/analyzer-protocol/src/index.ts | 24 +- 16 files changed, 1728 insertions(+), 51 deletions(-) create mode 100644 agents/capture-agent-py/capture_agent/signal_generator.py create mode 100644 agents/capture-agent-py/poetry.lock create mode 100644 apps/web/src/components/analyzer/SignalGeneratorSettings.tsx diff --git a/.github/workflows/build-installers.yml b/.github/workflows/build-installers.yml index dfd96f2..63533b0 100644 --- a/.github/workflows/build-installers.yml +++ b/.github/workflows/build-installers.yml @@ -180,9 +180,9 @@ jobs: CFBundleDisplayName SoundDocs Capture Agent CFBundleVersion - 0.1.13 + 0.2.0 CFBundleShortVersionString - 0.1.13 + 0.2.0 CFBundlePackageType APPL CFBundleIconFile @@ -196,7 +196,7 @@ jobs: EOF # Build package - pkgbuild --root pkg-root --identifier org.sounddocs.capture-agent --version 0.1.13 --install-location / SoundDocsAgent-macOS.pkg + pkgbuild --root pkg-root --identifier org.sounddocs.capture-agent --version 0.2.0 --install-location / SoundDocsAgent-macOS.pkg - name: Upload macOS installer uses: actions/upload-artifact@v4 @@ -244,7 +244,7 @@ jobs: cd agents/capture-agent-py cat > installer.nsi << 'EOF' !define APPNAME "SoundDocs Capture Agent" - !define APPVERSION "0.1.13" + !define APPVERSION "0.2.0" Name "${APPNAME}" OutFile "SoundDocsAgent-Windows.exe" InstallDir "$PROGRAMFILES\SoundDocs" diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fc3624..7d691dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.5.6.7] - 2025-09-25 + +### Added + +- **Signal Generator for Capture Agent**: Added comprehensive signal generation capabilities to the capture agent + - Pink Noise Generator: Professional-grade pink noise (1/f spectrum) using Voss-McCartney algorithm + - White Noise Generator: Full-spectrum white noise for system calibration + - Sine Wave Generator: Pure tone generation with adjustable frequency (20Hz - 20kHz) + - Sine Sweep Generator: Logarithmic sweep for system testing and alignment + - Click Track Generator: Metronome/timing reference with adjustable BPM + - Configurable output routing to any available audio channel + - Real-time signal generation with minimal latency + - WebSocket control interface for remote signal control from web UI +- **Internal Loopback Measurement**: Added loopback mode for measuring systems internally + - Routes generated signal directly to reference channel for internal system measurement + - Enables measuring DSP chains, plugins, and software signal paths without physical connections + - Useful for analyzing digital signal processing and software-based audio systems + +### Changed + +- **Capture Agent Version**: Bumped to `0.2.0` to reflect major feature addition +- **Web App Version**: Updated to `1.5.6.7` + ## [1.5.6.6] - 2025-09-21 ### Fixed diff --git a/README.md b/README.md index e9fb68d..c4adf5e 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ SoundDocs is a modern web application meticulously crafted for audio engineers, SoundDocs is provided free of charge and is open source under the AGPL v3 license. +**Privacy Promise**: Just because SoundDocs is free with no ads doesn't mean you are the product. We are committed to never selling, sharing, or monetizing your personal data. Your projects, documents, and information remain yours, always. + ![SoundDocs Hero Preview](https://i.postimg.cc/bJdc5Hmz/Screenshot-2025-06-05-at-19-21-01.png) ## ✨ Core Features diff --git a/agents/capture-agent-py/capture_agent/audio.py b/agents/capture-agent-py/capture_agent/audio.py index f2e5397..923b203 100644 --- a/agents/capture-agent-py/capture_agent/audio.py +++ b/agents/capture-agent-py/capture_agent/audio.py @@ -46,5 +46,5 @@ def blocks(self) -> Generator[np.ndarray, None, None]: # The `read` method returns a tuple (data, overflowed_boolean) data, overflowed = self.stream.read(self.config.blockSize) if overflowed: - print("Warning: Audio buffer overflowed.") + pass # Buffer overflow tracked yield data diff --git a/agents/capture-agent-py/capture_agent/dsp.py b/agents/capture-agent-py/capture_agent/dsp.py index 00e8bdd..5abb1cd 100644 --- a/agents/capture-agent-py/capture_agent/dsp.py +++ b/agents/capture-agent-py/capture_agent/dsp.py @@ -54,9 +54,9 @@ def log_memory_usage(): process = psutil.Process(os.getpid()) memory_info = process.memory_info() memory_mb = memory_info.rss / 1024 / 1024 - print(f"DSP Memory: {memory_mb:.1f} MB, Work arrays: {len(_work_arrays)}") - except Exception as e: - print(f"Memory monitoring error: {e}") + pass # Memory stats tracked + except Exception: + pass # Memory monitoring error def clear_dsp_caches(): """Clear DSP caches to free memory. @@ -670,7 +670,7 @@ def compute_metrics(block: np.ndarray, config: CaptureConfig) -> tuple[TFData, S fs = float(config.sampleRate) # Delay (linear GCC-PHAT you already implemented) - MAX_DELAY_MS = getattr(config, "maxDelayMs", 500.0) + MAX_DELAY_MS = getattr(config, "maxDelayMs", 2000.0) delay_ms, _ = _delay_pick_applied(x, y, fs, max_ms=MAX_DELAY_MS) # Integer align with zero-padding (preserve length) + fractional remainder diff --git a/agents/capture-agent-py/capture_agent/schema.py b/agents/capture-agent-py/capture_agent/schema.py index 23201dd..af67438 100644 --- a/agents/capture-agent-py/capture_agent/schema.py +++ b/agents/capture-agent-py/capture_agent/schema.py @@ -1,5 +1,5 @@ from pydantic import BaseModel, Field -from typing import List, Literal, Union +from typing import List, Literal, Union, Optional # Shared data structures class Device(BaseModel): @@ -23,13 +23,23 @@ class SPLData(BaseModel): AvgType = Literal["power", "linear", "exp"] LpfMode = Literal["lpf", "none"] +class SignalGeneratorConfig(BaseModel): + enabled: bool = False + signalType: Literal["sine", "white", "pink", "brown", "blue", "violet", "sine_sweep"] = "sine" + outputChannels: Optional[List[int]] = None # None means all channels + frequency: float = 1000.0 # Hz (for sine) + startFreq: float = 20.0 # Hz (for sweep) + endFreq: float = 20000.0 # Hz (for sweep) + sweepDuration: float = 1.0 # seconds (for sweep) + amplitude: float = 0.5 # 0.0 to 1.0 + class CaptureConfig(BaseModel): deviceId: str sampleRate: int blockSize: int refChan: int measChan: int - + # FFT & Averaging nfft: int avg: AvgType @@ -40,6 +50,10 @@ class CaptureConfig(BaseModel): lpfMode: LpfMode lpfFreq: float + # Signal Generator & Loopback + useLoopback: bool = False # Use loopback for reference channel + generator: Optional[SignalGeneratorConfig] = None + # Message types from client to agent class HelloMessage(BaseModel): type: Literal["hello"] @@ -72,6 +86,10 @@ class SetManualDelayMessage(BaseModel): type: Literal["set_manual_delay"] delay_ms: float | None = None +class UpdateGeneratorMessage(BaseModel): + type: Literal["update_generator"] + config: SignalGeneratorConfig + # Message types from agent to client class HelloAckMessage(BaseModel): type: Literal["hello_ack"] @@ -121,6 +139,7 @@ class CalibrationDoneMessage(BaseModel): GetVersionMessage, DelayFreezeMessage, SetManualDelayMessage, + UpdateGeneratorMessage, ] AgentMessage = Union[ diff --git a/agents/capture-agent-py/capture_agent/server.py b/agents/capture-agent-py/capture_agent/server.py index 9938b75..a6ed629 100644 --- a/agents/capture-agent-py/capture_agent/server.py +++ b/agents/capture-agent-py/capture_agent/server.py @@ -15,6 +15,7 @@ from . import __version__ from . import audio from . import dsp +from .signal_generator import SignalGenerator, SignalType, GeneratorConfig as GenConfig from .schema import ( CaptureConfig, ClientMessage, @@ -26,6 +27,8 @@ StartCaptureMessage, StoppedMessage, VersionMessage, + UpdateGeneratorMessage, + SignalGeneratorConfig, ) # --- State Management --- @@ -33,6 +36,8 @@ import weakref connected_clients: weakref.WeakSet = weakref.WeakSet() capture_task = None +signal_generator = None +generator_config = None ALLOWED_ORIGINS = ["https://sounddocs.org", "https://beta.sounddocs.org", "http://localhost:5173", "https://localhost:5173"] @@ -72,6 +77,7 @@ async def process_message(ws, message_data: dict): dsp.reset_dsp_state() dsp.clear_dsp_caches() # Ensure clean start config = CaptureConfig(**message.dict()) + # Config received, proceed with capture setup capture_task = asyncio.create_task(run_capture(ws, config)) # Add error handler for the task to prevent "Task exception was never retrieved" warnings def task_done_callback(task): @@ -107,11 +113,57 @@ def task_done_callback(task): **dsp.delay_status() })) + elif message.type == "update_generator": + global signal_generator, generator_config + generator_config = message.config + if generator_config.enabled: + # Create or update signal generator + gen_config = GenConfig( + signal_type=SignalType(generator_config.signalType), + sample_rate=48000, # Will be updated when capture starts + output_channels=generator_config.outputChannels, + frequency=generator_config.frequency, + start_freq=generator_config.startFreq, + end_freq=generator_config.endFreq, + sweep_duration=generator_config.sweepDuration, + amplitude=generator_config.amplitude + ) + signal_generator = SignalGenerator(gen_config) + else: + signal_generator = None + await ws.send(json.dumps({"type": "generator_updated", "enabled": generator_config.enabled if generator_config else False})) + async def run_capture(ws, config: CaptureConfig): + global signal_generator loop = asyncio.get_running_loop() aq: asyncio.Queue[np.ndarray] = asyncio.Queue(maxsize=128) # Increased from 32 to prevent frame drops num_channels = max(config.refChan, config.measChan) + # Initialize signal generator if configured + use_generator = False + if config.generator and config.generator.enabled: + # Initialize signal generator with config + gen_config = GenConfig( + signal_type=SignalType(config.generator.signalType), + sample_rate=config.sampleRate, + output_channels=config.generator.outputChannels, + frequency=config.generator.frequency, + start_freq=config.generator.startFreq, + end_freq=config.generator.endFreq, + sweep_duration=config.generator.sweepDuration, + amplitude=config.generator.amplitude + ) + signal_generator = SignalGenerator(gen_config) + use_generator = True + pass # Generator initialized + else: + pass # Generator not enabled + + # Get device info to determine capabilities early + device_info = sd.query_devices(int(config.deviceId)) + out_channels = device_info.get('max_output_channels', 0) + in_channels = device_info.get('max_input_channels', 0) + # Add dropped frame tracking dropped_frames = 0 last_drop_log_time = time.monotonic() @@ -120,16 +172,20 @@ async def run_capture(ws, config: CaptureConfig): # Dynamic buffer pool with better management initial_pool_size = 16 # Increased from 8 max_pool_size = 32 # Cap maximum pool growth - pool = deque([np.empty((1024, num_channels), dtype=np.float32) for _ in range(initial_pool_size)]) + blocksize = 4096 if (use_generator and out_channels > 0) else 1024 # Match stream blocksize + pool = deque([np.empty((blocksize, num_channels), dtype=np.float32) for _ in range(initial_pool_size)]) pool_miss_count = 0 last_gc_time = time.monotonic() gc_interval = 30.0 # Run GC hints every 30 seconds - def audio_callback(indata, frames, time_info, status): + # Buffer for generated signal if using loopback + generated_signal_buffer = None + + def audio_callback(indata, frames, _time_info, status): # called on driver thread; never block here - nonlocal pool_miss_count, dropped_frames + nonlocal pool_miss_count, dropped_frames, generated_signal_buffer if status: - print(f"Audio callback status: {status}") + pass # Audio callback status tracked buf = None # Fix 1: Ensure buffers are always returned to pool, even when frames are dropped @@ -139,12 +195,132 @@ def audio_callback(indata, frames, time_info, status): buf = pool.popleft() # PortAudio reuses its buffers; copy into our reusable buffer np.copyto(buf, indata, casting='no') + + # If using loopback, inject generated signal to reference channel + if config.useLoopback and generated_signal_buffer is not None and len(generated_signal_buffer) == frames: + # Replace reference channel with generated signal + ref_idx = config.refChan - 1 # Convert to 0-indexed + if ref_idx < buf.shape[1]: + buf[:, ref_idx] = generated_signal_buffer + except IndexError: # pool exhausted - track misses pool_miss_count += 1 # Create new buffer only if under max size if len(pool) < max_pool_size: buf = indata.copy() if indata.dtype == np.float32 else indata.astype(np.float32, copy=True) + + # Apply loopback even to newly created buffer + if config.useLoopback and generated_signal_buffer is not None and len(generated_signal_buffer) == frames: + ref_idx = config.refChan - 1 + if ref_idx < buf.shape[1]: + buf[:, ref_idx] = generated_signal_buffer + else: + # Pool at max, drop this frame to prevent unbounded growth + dropped_frames += 1 + return + + # Try to queue the buffer + loop.call_soon_threadsafe(aq.put_nowait, buf) + except Exception: + # queue full -> drop; always return buffer to pool + dropped_frames += 1 + if buf is not None and len(pool) < max_pool_size: + try: + pool.append(buf) + except Exception: + pass # If pool append fails, let GC handle it + finally: + # Critical fix: For buffers created during pool exhaustion that fail to queue, + # ensure they're returned to pool if there's space + pass # Buffer management handled in except block + + # Full-duplex callback for both input and output (macOS compatible) + duplex_callback_count = [0] + output_underrun_count = [0] + + def duplex_callback(indata, outdata, frames, _time_info, status): + nonlocal generated_signal_buffer, pool_miss_count, dropped_frames + duplex_callback_count[0] += 1 + + if duplex_callback_count[0] == 1: + pass # First callback initialized + + if status: + status_str = str(status).lower() + # Track output underruns specifically + if "output underflow" in status_str: + output_underrun_count[0] += 1 + if output_underrun_count[0] % 100 == 0: # Log every 100th underrun + pass # Underruns tracked + # Only log serious issues, not underflows which are common at startup + elif "underflow" not in status_str or duplex_callback_count[0] <= 2: + pass # Status tracked + + # Handle output (signal generation) first - CRITICAL for low latency + generated_signal = None + try: + if use_generator and signal_generator: + # Generate signal directly into output buffer for minimum latency + generated_signal = signal_generator.generate_block(frames, outdata.shape[1]) + + # Ensure signal is the right shape and type + if generated_signal.shape[0] == frames and generated_signal.shape[1] == outdata.shape[1]: + # Use faster memory copy + outdata[:] = generated_signal + else: + pass # Shape mismatch handled + outdata.fill(0) + + if duplex_callback_count[0] == 1: + pass # First signal generated + + # Store for loopback if needed + if config.useLoopback: + # Extract just the generated signal (not multichannel) for loopback + # Use the first active output channel's signal + if config.generator.outputChannels: + ch_idx = config.generator.outputChannels[0] - 1 if config.generator.outputChannels else 0 + else: + ch_idx = 0 + + if ch_idx < generated_signal.shape[1]: + generated_signal_buffer = generated_signal[:, ch_idx].copy() + else: + generated_signal_buffer = generated_signal[:, 0].copy() + else: + outdata.fill(0) + except Exception as e: + pass # Output error handled + outdata.fill(0) + + # Handle input (capture) processing + buf = None + try: + try: + buf = pool.popleft() + # PortAudio reuses its buffers; copy into our reusable buffer + np.copyto(buf, indata, casting='no') + + # If using loopback, inject generated signal to reference channel + if config.useLoopback and generated_signal_buffer is not None and len(generated_signal_buffer) == frames: + # Replace reference channel with generated signal + ref_idx = config.refChan - 1 # Convert to 0-indexed + if ref_idx < buf.shape[1]: + buf[:, ref_idx] = generated_signal_buffer + + except IndexError: + # pool exhausted - track misses + pool_miss_count += 1 + # Create new buffer only if under max size + if len(pool) < max_pool_size: + buf = indata.copy() if indata.dtype == np.float32 else indata.astype(np.float32, copy=True) + + # Apply loopback even to newly created buffer + if config.useLoopback and generated_signal_buffer is not None and len(generated_signal_buffer) == frames: + ref_idx = config.refChan - 1 + if ref_idx < buf.shape[1]: + buf[:, ref_idx] = generated_signal_buffer else: # Pool at max, drop this frame to prevent unbounded growth dropped_frames += 1 @@ -169,7 +345,7 @@ def audio_callback(indata, frames, time_info, status): try: fs = int(config.sampleRate) nperseg = int(config.nfft) - max_delay_ms = int(getattr(config, "maxDelayMs", 300)) + max_delay_ms = int(getattr(config, "maxDelayMs", 2000)) max_lag_samples = int(np.ceil(fs * max_delay_ms / 1000.0)) buffer_len = nperseg + 2*max_lag_samples + int(0.75*nperseg) @@ -182,16 +358,56 @@ def audio_callback(indata, frames, time_info, status): target_fps = 20.0 # UI update rate send_interval = 1.0 / target_fps - stream = sd.InputStream( - device=int(config.deviceId), - samplerate=config.sampleRate, - blocksize=1024, # <= smaller, smoother - channels=num_channels, - dtype="float32", - callback=audio_callback, - latency="high", # optional: reduce overruns - ) - stream.start() + # Device channels: input={in_channels}, output={out_channels} + + # Create stream based on whether we need output (signal generation) + if use_generator and out_channels > 0: + # Use full-duplex stream for macOS compatibility + # Creating full-duplex stream + try: + # Increase blocksize and latency for stable playback + blocksize = 4096 # Even larger block size for maximum stability + stream = sd.Stream( + device=int(config.deviceId), + samplerate=config.sampleRate, + blocksize=blocksize, + channels=(num_channels, out_channels), # (input_channels, output_channels) + dtype="float32", + callback=duplex_callback, + latency=("high", "high"), # Higher latency for both input and output for stability + prime_output_buffers_using_stream_callback=True, # Pre-fill output buffers + dither_off=True # Disable dithering for cleaner signal + ) + stream.start() + pass # Stream started + except Exception as e: + # Error starting full-duplex stream, falling back to input-only + stream = sd.InputStream( + device=int(config.deviceId), + samplerate=config.sampleRate, + blocksize=1024, + channels=num_channels, + dtype="float32", + callback=audio_callback, + latency="high", + ) + stream.start() + use_generator = False # Disable generator since we can't output + else: + # Input-only stream when no signal generation needed + # Creating input-only stream + stream = sd.InputStream( + device=int(config.deviceId), + samplerate=config.sampleRate, + blocksize=1024, + channels=num_channels, + dtype="float32", + callback=audio_callback, + latency="high", + ) + stream.start() + if use_generator: + pass # Generator unavailable for input-only device while True: # wait for at least one block @@ -235,12 +451,12 @@ def audio_callback(indata, frames, time_info, status): last_gc_time = now # Log pool health if we've had misses if pool_miss_count > 0: - print(f"Buffer pool health: {len(pool)} buffers, {pool_miss_count} misses since last check") + pass # Pool health tracked pool_miss_count = 0 # Log dropped frames periodically if dropped_frames > 0 and (now - last_drop_log_time > drop_log_interval or dropped_frames % 1000 == 0): - print(f"Dropped frames: {dropped_frames} total (queue full or pool exhausted)") + pass # Frame drops tracked last_drop_log_time = now # run analysis only when we've advanced by one hop @@ -259,7 +475,7 @@ def audio_callback(indata, frames, time_info, status): tf=tf_data, spl=spl_data, delay_ms=applied, # show applied, not local variable - latency_ms=float(stream.latency)*1000.0 if hasattr(stream, "latency") else 0.0, + latency_ms=float(stream.latency[0] if isinstance(stream.latency, tuple) else stream.latency)*1000.0 if hasattr(stream, "latency") else 0.0, ts=int(time.time() * 1000), sampleRate=fs, delay_mode=status["mode"], @@ -268,16 +484,16 @@ def audio_callback(indata, frames, time_info, status): await ws.send(frame.model_dump_json()) last_send = now except websockets.exceptions.ConnectionClosed: - print("WebSocket connection closed during frame send") + pass # Connection closed during send break # Exit the capture loop else: - print("WebSocket connection is not open, stopping capture") + # WebSocket not open, stopping break carry -= hop_size await asyncio.sleep(0) except asyncio.CancelledError: - print("Capture task cancelled") + pass # Task cancelled except Exception as e: print(f"Error during capture: {e}") # Only send error if connection is still open @@ -312,7 +528,7 @@ def audio_callback(indata, frames, time_info, status): # Log final dropped frames count if any if dropped_frames > 0: - print(f"Capture ended with {dropped_frames} total dropped frames") + pass # Capture completed async def send_error(ws, error_message: str): error_msg = ErrorMessage(type="error", message=error_message) @@ -326,9 +542,9 @@ async def handler(ws): # Allow connections without Origin header (local/direct connections) # or from allowed origins (browser connections) if origin is None: - print("Client connected from local/direct connection (no Origin header)") + pass # Local connection elif origin in ALLOWED_ORIGINS: - print(f"Client connected from allowed origin: {origin}") + pass # Allowed origin else: print(f"Connection rejected from disallowed origin: {origin}") return @@ -344,7 +560,7 @@ async def handler(ws): except Exception as e: await send_error(ws, f"An unexpected error occurred: {e}") except websockets.exceptions.ConnectionClosed: - print("Client disconnected.") + pass # Client disconnected except Exception as e: print(f"Unexpected error in WebSocket handler: {e}") finally: diff --git a/agents/capture-agent-py/capture_agent/signal_generator.py b/agents/capture-agent-py/capture_agent/signal_generator.py new file mode 100644 index 0000000..377025a --- /dev/null +++ b/agents/capture-agent-py/capture_agent/signal_generator.py @@ -0,0 +1,269 @@ +import numpy as np +try: + import colorednoise as cn + COLOREDNOISE_AVAILABLE = True +except ImportError: + print("WARNING: colorednoise not installed. Colored noise generation will not work.") + COLOREDNOISE_AVAILABLE = False +from typing import Optional, List +from dataclasses import dataclass +from enum import Enum + +class SignalType(Enum): + SINE = "sine" + WHITE_NOISE = "white" + PINK_NOISE = "pink" + BROWN_NOISE = "brown" + BLUE_NOISE = "blue" + VIOLET_NOISE = "violet" + SINE_SWEEP = "sine_sweep" + +@dataclass +class GeneratorConfig: + signal_type: SignalType + sample_rate: int = 48000 + output_channels: List[int] = None # Which channels to output to, None = all + # Sine specific + frequency: float = 1000.0 # Hz + # Sine sweep specific + start_freq: float = 20.0 # Hz + end_freq: float = 20000.0 # Hz + sweep_duration: float = 1.0 # seconds + # Noise specific + noise_color: Optional[float] = None # Beta parameter for colorednoise + # General + amplitude: float = 0.5 # 0.0 to 1.0 + +class SignalGenerator: + """Generates various test signals for audio measurement.""" + + def __init__(self, config: GeneratorConfig): + self.config = config + self.sample_rate = config.sample_rate + self.phase = 0.0 # For sine wave continuity + self.sweep_phase = 0.0 # For sweep continuity + self.sweep_start_time = 0.0 + self.current_time = 0.0 + self._initialized = False + + # Map noise types to beta values for colorednoise + self.noise_beta_map = { + SignalType.WHITE_NOISE: 0, # 1/f^0 + SignalType.PINK_NOISE: 1, # 1/f^1 + SignalType.BROWN_NOISE: 2, # 1/f^2 + SignalType.BLUE_NOISE: -1, # f^1 + SignalType.VIOLET_NOISE: -2, # f^2 + } + + # Pre-generated noise table for smooth playback + self._noise_table = None + self._noise_len = 0 + self._noise_pos = 0 + self._xfade_len = 2048 # ~43 ms @48kHz for smooth crossfade + self._xfade_in = None + self._xfade_out = None + + # Pre-generate noise table if needed + if self.config.signal_type in self.noise_beta_map: + self._prepare_colored_noise_table(seconds=60) # 60s loop, efficient at runtime + + # Pre-generate a small block to initialize the generator + self._initialize_generator() + + def _prepare_colored_noise_table(self, seconds: int = 60): + """Build a long colored-noise table once, not in the callback.""" + N = int(self.sample_rate * seconds) + beta = self.noise_beta_map.get(self.config.signal_type, 0) + + if self.config.signal_type == SignalType.BROWN_NOISE: + # Brown: leaky integrator to avoid drift + white = np.random.randn(N).astype(np.float32) + # Leaky integration (approx 1/f^2 without unbounded DC) + alpha = 0.9995 + y = np.empty_like(white) + acc = 0.0 + for i in range(N): + acc = alpha * acc + (1.0 - alpha) * white[i] + y[i] = acc + x = y + else: + if COLOREDNOISE_AVAILABLE and beta != 0: + x = cn.powerlaw_psd_gaussian(beta, N).astype(np.float32) + else: + x = np.random.randn(N).astype(np.float32) + + # Normalize once (no per-block pumping) + rms = np.sqrt(np.mean(x**2)) + if rms > 1e-12: + x = x / rms * 0.5 # Scale to reasonable level + + self._noise_table = x + self._noise_len = N + self._noise_pos = 0 + + # Hann crossfade windows for seamless loop + L = min(self._xfade_len, max(64, N // 64)) + w = 0.5 - 0.5 * np.cos(2 * np.pi * np.arange(2 * L) / (2 * L - 1)) + self._xfade_in = w[:L].astype(np.float32) + self._xfade_out = w[L:].astype(np.float32) + self._xfade_len = L + + def generate_block(self, block_size: int, num_channels: int) -> np.ndarray: + """Generate a block of signal data. + + Args: + block_size: Number of samples per channel + num_channels: Total number of channels to generate + + Returns: + numpy array of shape (block_size, num_channels) + """ + # Create output array + output = np.zeros((block_size, num_channels), dtype=np.float32) + + # Generate the signal + if self.config.signal_type == SignalType.SINE: + signal = self._generate_sine(block_size) + elif self.config.signal_type == SignalType.SINE_SWEEP: + signal = self._generate_sine_sweep(block_size) + elif self.config.signal_type in self.noise_beta_map: + signal = self._generate_colored_noise(block_size) + else: + pass # Unknown signal type, returning zeros + signal = np.zeros(block_size, dtype=np.float32) + + # Apply amplitude scaling + signal *= self.config.amplitude + + # Debug: Check if signal has content + if np.max(np.abs(signal)) == 0: + pass # Signal is silent but this may be intentional + + # Route to specified channels + channels = self.config.output_channels + if channels is None: + # Output to all channels + for ch in range(num_channels): + output[:, ch] = signal + else: + # Output only to specified channels (1-indexed) + for ch in channels: + if 1 <= ch <= num_channels: + output[:, ch - 1] = signal + + return output + + def _generate_sine(self, block_size: int) -> np.ndarray: + """Generate a sine wave signal.""" + t = np.arange(block_size) / self.sample_rate + signal = np.sin(2 * np.pi * self.config.frequency * t + self.phase) + + # Update phase for continuity + self.phase += 2 * np.pi * self.config.frequency * block_size / self.sample_rate + self.phase = np.fmod(self.phase, 2 * np.pi) # Keep phase within 0-2π + + return signal.astype(np.float32) + + def _generate_sine_sweep(self, block_size: int) -> np.ndarray: + """Generate a logarithmic sine sweep signal.""" + t = np.arange(block_size) / self.sample_rate + + # Calculate sweep parameters + duration = self.config.sweep_duration + f0 = self.config.start_freq + f1 = self.config.end_freq + + # Create time array relative to sweep start + sweep_times = t + self.current_time + + # Reset sweep when we reach the end + sweep_times = np.fmod(sweep_times, duration) + + # Logarithmic frequency sweep + # f(t) = f0 * (f1/f0)^(t/T) + k = np.log(f1 / f0) / duration + frequencies = f0 * np.exp(k * sweep_times) + + # Generate swept sine using phase accumulation for continuity + signal = np.zeros(block_size, dtype=np.float32) + for i in range(block_size): + signal[i] = np.sin(self.sweep_phase) + # Update phase based on instantaneous frequency + self.sweep_phase += 2 * np.pi * frequencies[i] / self.sample_rate + + # Reset phase when sweep restarts + if i > 0 and sweep_times[i] < sweep_times[i-1]: + self.sweep_phase = 0.0 + + # Keep phase within bounds + self.sweep_phase = np.fmod(self.sweep_phase, 2 * np.pi) + + # Update current time + self.current_time += block_size / self.sample_rate + if self.current_time >= duration: + self.current_time = np.fmod(self.current_time, duration) + + return signal + + def _generate_colored_noise(self, block_size: int) -> np.ndarray: + """Read from pre-generated noise table with seamless looping.""" + if self._noise_table is None or self._noise_len == 0: + # Fallback (shouldn't happen): one-time build + self._prepare_colored_noise_table(seconds=60) + + p = self._noise_pos + N = self._noise_len + L = self._xfade_len + + if p + block_size <= N: + # Simple case: read straight from table + out = self._noise_table[p:p + block_size].copy() + p += block_size + if p == N: + p = 0 # Wrap to beginning + else: + # Wrap case: need to read from end and beginning + n1 = N - p # Samples from end + n2 = block_size - n1 # Samples from beginning + out = np.empty(block_size, dtype=np.float32) + out[:n1] = self._noise_table[p:] + out[n1:] = self._noise_table[:n2] + + # Apply crossfade at the loop seam for click-free playback + if n1 >= L and n2 >= L: + # Crossfade last L samples of part1 with first L samples of part2 + fade_out_portion = self._noise_table[N - L:N] * self._xfade_out + fade_in_portion = self._noise_table[:L] * self._xfade_in + # The crossfade happens at the boundary + crossfaded = fade_out_portion + fade_in_portion + # Replace the boundary region with crossfaded version + if n1 >= L: + out[n1 - L:n1] = crossfaded + else: + # Handle edge case where n1 < L + out[:n1] = crossfaded[-n1:] + + p = n2 # New position after wrap + + self._noise_pos = p + + # NO per-block normalization (that was causing pumping/choppiness) + # The table is already normalized, just return it + return out + + def _initialize_generator(self): + """Pre-initialize the generator by generating a small block.""" + if not self._initialized: + # Generate a small block to warm up the generator + # This ensures the first real callback doesn't generate silence + _ = self.generate_block(64, 2) + self._initialized = True + pass # Generator initialized + + def reset(self): + """Reset generator state (phases, etc).""" + self.phase = 0.0 + self.sweep_phase = 0.0 + self.current_time = 0.0 + self.sweep_start_time = 0.0 + self._noise_pos = 0 # Reset noise table position \ No newline at end of file diff --git a/agents/capture-agent-py/poetry.lock b/agents/capture-agent-py/poetry.lock new file mode 100644 index 0000000..76d34e1 --- /dev/null +++ b/agents/capture-agent-py/poetry.lock @@ -0,0 +1,688 @@ +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "cffi" +version = "2.0.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + +[[package]] +name = "colorednoise" +version = "2.2.0" +description = "Generate Gaussian (1/f)**beta noise (e.g. pink noise)" +optional = false +python-versions = ">=3" +groups = ["main"] +files = [ + {file = "colorednoise-2.2.0-py3-none-any.whl", hash = "sha256:7a39faab851889bd742668d3ba1502eec6a72008d6e004bd8fa6313355c7b487"}, + {file = "colorednoise-2.2.0.tar.gz", hash = "sha256:6ffe0ab1baf3355df28cde497d0affb0fb0ef1cd0a0c41302e0d7e7814c187b6"}, +] + +[package.dependencies] +numpy = ">=1.17.0" + +[[package]] +name = "cryptography" +version = "43.0.3" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, + {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, + {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, + {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, + {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, + {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, + {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "numpy" +version = "2.3.3" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.11" +groups = ["main"] +files = [ + {file = "numpy-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d"}, + {file = "numpy-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569"}, + {file = "numpy-2.3.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f"}, + {file = "numpy-2.3.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125"}, + {file = "numpy-2.3.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48"}, + {file = "numpy-2.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6"}, + {file = "numpy-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa"}, + {file = "numpy-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30"}, + {file = "numpy-2.3.3-cp311-cp311-win32.whl", hash = "sha256:eb63d443d7b4ffd1e873f8155260d7f58e7e4b095961b01c91062935c2491e57"}, + {file = "numpy-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:ec9d249840f6a565f58d8f913bccac2444235025bbb13e9a4681783572ee3caa"}, + {file = "numpy-2.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:74c2a948d02f88c11a3c075d9733f1ae67d97c6bdb97f2bb542f980458b257e7"}, + {file = "numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf"}, + {file = "numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25"}, + {file = "numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe"}, + {file = "numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b"}, + {file = "numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8"}, + {file = "numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20"}, + {file = "numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea"}, + {file = "numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7"}, + {file = "numpy-2.3.3-cp312-cp312-win32.whl", hash = "sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf"}, + {file = "numpy-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb"}, + {file = "numpy-2.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5"}, + {file = "numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf"}, + {file = "numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7"}, + {file = "numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6"}, + {file = "numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7"}, + {file = "numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c"}, + {file = "numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93"}, + {file = "numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae"}, + {file = "numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86"}, + {file = "numpy-2.3.3-cp313-cp313-win32.whl", hash = "sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8"}, + {file = "numpy-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf"}, + {file = "numpy-2.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5"}, + {file = "numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc"}, + {file = "numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc"}, + {file = "numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b"}, + {file = "numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19"}, + {file = "numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30"}, + {file = "numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e"}, + {file = "numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3"}, + {file = "numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea"}, + {file = "numpy-2.3.3-cp313-cp313t-win32.whl", hash = "sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd"}, + {file = "numpy-2.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d"}, + {file = "numpy-2.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1"}, + {file = "numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593"}, + {file = "numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652"}, + {file = "numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7"}, + {file = "numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a"}, + {file = "numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe"}, + {file = "numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421"}, + {file = "numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021"}, + {file = "numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf"}, + {file = "numpy-2.3.3-cp314-cp314-win32.whl", hash = "sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0"}, + {file = "numpy-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8"}, + {file = "numpy-2.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe"}, + {file = "numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00"}, + {file = "numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a"}, + {file = "numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d"}, + {file = "numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a"}, + {file = "numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54"}, + {file = "numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e"}, + {file = "numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097"}, + {file = "numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970"}, + {file = "numpy-2.3.3-cp314-cp314t-win32.whl", hash = "sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5"}, + {file = "numpy-2.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f"}, + {file = "numpy-2.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc"}, + {file = "numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029"}, +] + +[[package]] +name = "pycparser" +version = "2.23" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, + {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, +] + +[[package]] +name = "pydantic" +version = "2.11.9" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2"}, + {file = "pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.33.2" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pyfftw" +version = "0.14.0" +description = "A pythonic wrapper around FFTW, the FFT library, presenting a unified interface for all the supported transforms." +optional = true +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pyFFTW-0.14.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0875da4116c5b85a9df9533d4d4b35c4f408c57b0103ad26fb5e5cc845b40923"}, + {file = "pyFFTW-0.14.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:d94cd827c8716f3c75b8f2947d2289fbd551e039f2bba970633357e9905ddbb7"}, + {file = "pyFFTW-0.14.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ebecbbb38e2e13654782e344d9490e5bee9e9e83e45539192aa086f9600afce3"}, + {file = "pyFFTW-0.14.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:78e25340a9d36a78d657d5d758f658e8a29563760c19d25eb075399d03dad8ea"}, + {file = "pyFFTW-0.14.0-cp310-cp310-win32.whl", hash = "sha256:7cd13afbc2e8afdcca5775de2e1d5a56175869899eff1a141aef8897144565f8"}, + {file = "pyFFTW-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:65aecb2e96316e405dc994affc7228b8ab3d387645257b741e69d48df51a975a"}, + {file = "pyFFTW-0.14.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:72ca73b50a9015e4e5370b4fbab36e48db23129ba2728936fc4420d4cf94af60"}, + {file = "pyFFTW-0.14.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:9b4113a8810a3375adc30d9e94c257530efd715f633faffddab51723406a311f"}, + {file = "pyFFTW-0.14.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e8eb372a3f8f6786239e6ee253c16898ff76be339be40d2192ec5802e02d59e5"}, + {file = "pyFFTW-0.14.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ffbcdb49d4d961ff703b889208838ded0b47f3df36ff205358a737e525815962"}, + {file = "pyFFTW-0.14.0-cp311-cp311-win32.whl", hash = "sha256:fa3425d216bc83e51d81526f232ad9027e9cbc7247b7017f44b59f5889ea50e8"}, + {file = "pyFFTW-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:1ac9768f2a9fdbe18bbb63185c71d78097ee7df7f7e321b22c1faf540f7176e1"}, + {file = "pyFFTW-0.14.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:fd1eb00d286d5d6e852b472b661e99151bea1444c55c6a40fca9f9b97d437484"}, + {file = "pyFFTW-0.14.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:533819b1145c92fa33b16ecdd7f234c3ea62d2cc0c1da35cd3beee4695286435"}, + {file = "pyFFTW-0.14.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:69ca338d5c2e519c1ed9c5ecbafa8508d0c35fe38d0aa08ca665c4fb7206b28e"}, + {file = "pyFFTW-0.14.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:2ca9aa7f76c9dd7f4813c8975359f2a655a4132b93f08176ff70376a3d7edc59"}, + {file = "pyFFTW-0.14.0-cp312-cp312-win32.whl", hash = "sha256:66d000b6bd7d6de5f1e53cc590481789293812ef9283493211fe677b7fd0aabf"}, + {file = "pyFFTW-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:c961044f12282a7267db40b279cee86ace97e1d05a4bbec9e01a646ccdd400fb"}, + {file = "pyFFTW-0.14.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:c3be6188ba0441d963333df7db29e661b324d870156d8f81efe9003ad447ed9b"}, + {file = "pyFFTW-0.14.0-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:72aa2bf213e68e69755dcd3ef76cb05ac9fc82960c51fff902af8f01240564a4"}, + {file = "pyFFTW-0.14.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:76a82f3502a3c3d0f9d3919f488afe5e948eb0e614b097dc9f3a347e3eeaf967"}, + {file = "pyFFTW-0.14.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:0889c3fa9acbf5f9b9b5dac7d8352ee11f9aaba91e3bfa3cd5d4c236e49ca940"}, + {file = "pyFFTW-0.14.0-cp39-cp39-win32.whl", hash = "sha256:6d4f70fbf5ed298cf946614770206ff16abefc34df1e92ba21bed5d3df5b1c80"}, + {file = "pyFFTW-0.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:14c3547eeb22a897aedc0e4c4654dde52d8564ecee9f8b5664900974e4e8c006"}, + {file = "pyFFTW-0.14.0-pp39-pypy39_pp73-macosx_12_0_x86_64.whl", hash = "sha256:5631d8a5b74437d3dd8b39eb019b4bf269193726f9a5da33e64bb1d2856040d5"}, + {file = "pyFFTW-0.14.0-pp39-pypy39_pp73-macosx_14_0_arm64.whl", hash = "sha256:348eef83dadc46bf3132a6fd39b3bf3794b417fb7e4938027d988e94f85ef026"}, + {file = "pyFFTW-0.14.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7a317ce8059d35fb5076dc7575d67eca664f9fcd834320ea05982c463bd4e9fc"}, + {file = "pyFFTW-0.14.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4ab9f5cd4103683d9631f149c7df0c9be5841b1bd157cb1c94618269283a8f46"}, + {file = "pyfftw-0.14.0.tar.gz", hash = "sha256:a55f94d3da9b5c04de1bc96932a93f922910f3984557931356173a515277b65b"}, +] + +[package.dependencies] +numpy = ">=1.20" +setuptools = ">=70.1.1" + +[package.extras] +dask = ["dask[array] (>=1.0)", "numpy (>=1.20)"] +scipy = ["scipy (>=1.8.0)"] + +[[package]] +name = "scipy" +version = "1.16.2" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = ">=3.11" +groups = ["main"] +files = [ + {file = "scipy-1.16.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:6ab88ea43a57da1af33292ebd04b417e8e2eaf9d5aa05700be8d6e1b6501cd92"}, + {file = "scipy-1.16.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c95e96c7305c96ede73a7389f46ccd6c659c4da5ef1b2789466baeaed3622b6e"}, + {file = "scipy-1.16.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:87eb178db04ece7c698220d523c170125dbffebb7af0345e66c3554f6f60c173"}, + {file = "scipy-1.16.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:4e409eac067dcee96a57fbcf424c13f428037827ec7ee3cb671ff525ca4fc34d"}, + {file = "scipy-1.16.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e574be127bb760f0dad24ff6e217c80213d153058372362ccb9555a10fc5e8d2"}, + {file = "scipy-1.16.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5db5ba6188d698ba7abab982ad6973265b74bb40a1efe1821b58c87f73892b9"}, + {file = "scipy-1.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec6e74c4e884104ae006d34110677bfe0098203a3fec2f3faf349f4cb05165e3"}, + {file = "scipy-1.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:912f46667d2d3834bc3d57361f854226475f695eb08c08a904aadb1c936b6a88"}, + {file = "scipy-1.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:91e9e8a37befa5a69e9cacbe0bcb79ae5afb4a0b130fd6db6ee6cc0d491695fa"}, + {file = "scipy-1.16.2-cp311-cp311-win_arm64.whl", hash = "sha256:f3bf75a6dcecab62afde4d1f973f1692be013110cad5338007927db8da73249c"}, + {file = "scipy-1.16.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:89d6c100fa5c48472047632e06f0876b3c4931aac1f4291afc81a3644316bb0d"}, + {file = "scipy-1.16.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ca748936cd579d3f01928b30a17dc474550b01272d8046e3e1ee593f23620371"}, + {file = "scipy-1.16.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:fac4f8ce2ddb40e2e3d0f7ec36d2a1e7f92559a2471e59aec37bd8d9de01fec0"}, + {file = "scipy-1.16.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:033570f1dcefd79547a88e18bccacff025c8c647a330381064f561d43b821232"}, + {file = "scipy-1.16.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ea3421209bf00c8a5ef2227de496601087d8f638a2363ee09af059bd70976dc1"}, + {file = "scipy-1.16.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f66bd07ba6f84cd4a380b41d1bf3c59ea488b590a2ff96744845163309ee8e2f"}, + {file = "scipy-1.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e9feab931bd2aea4a23388c962df6468af3d808ddf2d40f94a81c5dc38f32ef"}, + {file = "scipy-1.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03dfc75e52f72cf23ec2ced468645321407faad8f0fe7b1f5b49264adbc29cb1"}, + {file = "scipy-1.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:0ce54e07bbb394b417457409a64fd015be623f36e330ac49306433ffe04bc97e"}, + {file = "scipy-1.16.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a8ffaa4ac0df81a0b94577b18ee079f13fecdb924df3328fc44a7dc5ac46851"}, + {file = "scipy-1.16.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:84f7bf944b43e20b8a894f5fe593976926744f6c185bacfcbdfbb62736b5cc70"}, + {file = "scipy-1.16.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5c39026d12edc826a1ef2ad35ad1e6d7f087f934bb868fc43fa3049c8b8508f9"}, + {file = "scipy-1.16.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e52729ffd45b68777c5319560014d6fd251294200625d9d70fd8626516fc49f5"}, + {file = "scipy-1.16.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:024dd4a118cccec09ca3209b7e8e614931a6ffb804b2a601839499cb88bdf925"}, + {file = "scipy-1.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7a5dc7ee9c33019973a470556081b0fd3c9f4c44019191039f9769183141a4d9"}, + {file = "scipy-1.16.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c2275ff105e508942f99d4e3bc56b6ef5e4b3c0af970386ca56b777608ce95b7"}, + {file = "scipy-1.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:af80196eaa84f033e48444d2e0786ec47d328ba00c71e4299b602235ffef9acb"}, + {file = "scipy-1.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9fb1eb735fe3d6ed1f89918224e3385fbf6f9e23757cacc35f9c78d3b712dd6e"}, + {file = "scipy-1.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:fda714cf45ba43c9d3bae8f2585c777f64e3f89a2e073b668b32ede412d8f52c"}, + {file = "scipy-1.16.2-cp313-cp313-win_arm64.whl", hash = "sha256:2f5350da923ccfd0b00e07c3e5cfb316c1c0d6c1d864c07a72d092e9f20db104"}, + {file = "scipy-1.16.2-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:53d8d2ee29b925344c13bda64ab51785f016b1b9617849dac10897f0701b20c1"}, + {file = "scipy-1.16.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:9e05e33657efb4c6a9d23bd8300101536abd99c85cca82da0bffff8d8764d08a"}, + {file = "scipy-1.16.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:7fe65b36036357003b3ef9d37547abeefaa353b237e989c21027b8ed62b12d4f"}, + {file = "scipy-1.16.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6406d2ac6d40b861cccf57f49592f9779071655e9f75cd4f977fa0bdd09cb2e4"}, + {file = "scipy-1.16.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff4dc42bd321991fbf611c23fc35912d690f731c9914bf3af8f417e64aca0f21"}, + {file = "scipy-1.16.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:654324826654d4d9133e10675325708fb954bc84dae6e9ad0a52e75c6b1a01d7"}, + {file = "scipy-1.16.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63870a84cd15c44e65220eaed2dac0e8f8b26bbb991456a033c1d9abfe8a94f8"}, + {file = "scipy-1.16.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fa01f0f6a3050fa6a9771a95d5faccc8e2f5a92b4a2e5440a0fa7264a2398472"}, + {file = "scipy-1.16.2-cp313-cp313t-win_amd64.whl", hash = "sha256:116296e89fba96f76353a8579820c2512f6e55835d3fad7780fece04367de351"}, + {file = "scipy-1.16.2-cp313-cp313t-win_arm64.whl", hash = "sha256:98e22834650be81d42982360382b43b17f7ba95e0e6993e2a4f5b9ad9283a94d"}, + {file = "scipy-1.16.2-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:567e77755019bb7461513c87f02bb73fb65b11f049aaaa8ca17cfaa5a5c45d77"}, + {file = "scipy-1.16.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:17d9bb346194e8967296621208fcdfd39b55498ef7d2f376884d5ac47cec1a70"}, + {file = "scipy-1.16.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:0a17541827a9b78b777d33b623a6dcfe2ef4a25806204d08ead0768f4e529a88"}, + {file = "scipy-1.16.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:d7d4c6ba016ffc0f9568d012f5f1eb77ddd99412aea121e6fa8b4c3b7cbad91f"}, + {file = "scipy-1.16.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9702c4c023227785c779cba2e1d6f7635dbb5b2e0936cdd3a4ecb98d78fd41eb"}, + {file = "scipy-1.16.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1cdf0ac28948d225decdefcc45ad7dd91716c29ab56ef32f8e0d50657dffcc7"}, + {file = "scipy-1.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:70327d6aa572a17c2941cdfb20673f82e536e91850a2e4cb0c5b858b690e1548"}, + {file = "scipy-1.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5221c0b2a4b58aa7c4ed0387d360fd90ee9086d383bb34d9f2789fafddc8a936"}, + {file = "scipy-1.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:f5a85d7b2b708025af08f060a496dd261055b617d776fc05a1a1cc69e09fe9ff"}, + {file = "scipy-1.16.2-cp314-cp314-win_arm64.whl", hash = "sha256:2cc73a33305b4b24556957d5857d6253ce1e2dcd67fa0ff46d87d1670b3e1e1d"}, + {file = "scipy-1.16.2-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:9ea2a3fed83065d77367775d689401a703d0f697420719ee10c0780bcab594d8"}, + {file = "scipy-1.16.2-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7280d926f11ca945c3ef92ba960fa924e1465f8d07ce3a9923080363390624c4"}, + {file = "scipy-1.16.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:8afae1756f6a1fe04636407ef7dbece33d826a5d462b74f3d0eb82deabefd831"}, + {file = "scipy-1.16.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:5c66511f29aa8d233388e7416a3f20d5cae7a2744d5cee2ecd38c081f4e861b3"}, + {file = "scipy-1.16.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efe6305aeaa0e96b0ccca5ff647a43737d9a092064a3894e46c414db84bc54ac"}, + {file = "scipy-1.16.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f3a337d9ae06a1e8d655ee9d8ecb835ea5ddcdcbd8d23012afa055ab014f374"}, + {file = "scipy-1.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bab3605795d269067d8ce78a910220262711b753de8913d3deeaedb5dded3bb6"}, + {file = "scipy-1.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b0348d8ddb55be2a844c518cd8cc8deeeb8aeba707cf834db5758fc89b476a2c"}, + {file = "scipy-1.16.2-cp314-cp314t-win_amd64.whl", hash = "sha256:26284797e38b8a75e14ea6631d29bda11e76ceaa6ddb6fdebbfe4c4d90faf2f9"}, + {file = "scipy-1.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d2a4472c231328d4de38d5f1f68fdd6d28a615138f842580a8a321b5845cf779"}, + {file = "scipy-1.16.2.tar.gz", hash = "sha256:af029b153d243a80afb6eabe40b0a07f8e35c9adc269c019f364ad747f826a6b"}, +] + +[package.dependencies] +numpy = ">=1.25.2,<2.6" + +[package.extras] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] +doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.19.1)", "jupytext", "linkify-it-py", "matplotlib (>=3.5)", "myst-nb (>=1.2.0)", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.2.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict (>=2.3.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja ; sys_platform != \"emscripten\"", "pooch", "pytest (>=8.0.0)", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + +[[package]] +name = "setuptools" +version = "80.9.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = true +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, + {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] + +[[package]] +name = "sounddevice" +version = "0.4.7" +description = "Play and Record Sound with Python" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "sounddevice-0.4.7-py3-none-any.whl", hash = "sha256:1c3f18bfa4d9a257f5715f2ab83f2c0eb412a09f3e6a9fa73720886ca88f6bc7"}, + {file = "sounddevice-0.4.7-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:d6ddfd341ad7412b14ca001f2c4dbf5fa2503bdc9eb15ad2c3105f6c260b698a"}, + {file = "sounddevice-0.4.7-py3-none-win32.whl", hash = "sha256:1ec1df094c468a210113aa22c4f390d5b4d9c7a73e41a6cb6ecfec83db59b380"}, + {file = "sounddevice-0.4.7-py3-none-win_amd64.whl", hash = "sha256:0c8b3543da1496f282b66a7bc54b755577ba638b1af06c146d4ac7f39d86b548"}, + {file = "sounddevice-0.4.7.tar.gz", hash = "sha256:69b386818d50a2d518607d4b973442e8d524760c7cd6c8b8be03d8c98fc4bce7"}, +] + +[package.dependencies] +CFFI = ">=1.0" + +[package.extras] +numpy = ["NumPy"] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + +[[package]] +name = "websockets" +version = "12.0" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"}, + {file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"}, + {file = "websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603"}, + {file = "websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f"}, + {file = "websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f"}, + {file = "websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53"}, + {file = "websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402"}, + {file = "websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"}, + {file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"}, + {file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"}, + {file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"}, + {file = "websockets-12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438"}, + {file = "websockets-12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2"}, + {file = "websockets-12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7"}, + {file = "websockets-12.0-cp38-cp38-win32.whl", hash = "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62"}, + {file = "websockets-12.0-cp38-cp38-win_amd64.whl", hash = "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892"}, + {file = "websockets-12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d"}, + {file = "websockets-12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28"}, + {file = "websockets-12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9"}, + {file = "websockets-12.0-cp39-cp39-win32.whl", hash = "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6"}, + {file = "websockets-12.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8"}, + {file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b"}, + {file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"}, + {file = "websockets-12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2"}, + {file = "websockets-12.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468"}, + {file = "websockets-12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611"}, + {file = "websockets-12.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370"}, + {file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"}, + {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, +] + +[metadata] +lock-version = "2.1" +python-versions = "^3.11" +content-hash = "91b544f453151a0d98c23895c7b212fe261be7114df4d8912bd409c4ccd9482f" diff --git a/agents/capture-agent-py/pyproject.toml b/agents/capture-agent-py/pyproject.toml index 2bfaeb8..2e01f52 100644 --- a/agents/capture-agent-py/pyproject.toml +++ b/agents/capture-agent-py/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "capture-agent" -version = "0.1.14" +version = "0.2.0" description = "SoundDocs Capture Agent for multi-channel audio analysis." authors = ["CJ Vana cj@sounddocs.org"] readme = "README.md" @@ -11,9 +11,10 @@ websockets = "^12.0" sounddevice = "^0.4.6" numpy = "^2.3.2" scipy = "^1.16.1" -pyfftw = "^0.14.0" +pyfftw = { version = "^0.14.0", optional = true } pydantic = "^2.7.1" cryptography = ">=41,<44" +colorednoise = "^2.2.0" [build-system] requires = ["poetry-core"] diff --git a/apps/web/index.html b/apps/web/index.html index f8861c5..84ffea8 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -85,7 +85,7 @@ "Resource Hub: Reference Guides (Pinouts, Frequency Bands, dB Charts)" ], "operatingSystem": "Web", - "softwareVersion": "1.5.6.6", // Update as your app versions + "softwareVersion": "1.5.6.7", // Update as your app versions "offers": { "@type": "Offer", "price": "0", diff --git a/apps/web/package.json b/apps/web/package.json index bbee75b..0eb395e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,7 +1,7 @@ { "name": "@sounddocs/web", "private": true, - "version": "1.5.6.6", + "version": "1.5.6.7", "type": "module", "scripts": { "dev": "vite", diff --git a/apps/web/src/components/analyzer/ProSettings.tsx b/apps/web/src/components/analyzer/ProSettings.tsx index 1a06d41..c988a1c 100644 --- a/apps/web/src/components/analyzer/ProSettings.tsx +++ b/apps/web/src/components/analyzer/ProSettings.tsx @@ -10,6 +10,9 @@ interface ProSettingsProps { delayMode?: string; appliedDelayMs?: number; isCapturing: boolean; + signalGeneratorConfig?: any; + selectedDeviceId?: string; + onDeviceSelect?: (deviceId: string) => void; } export const ProSettings: React.FC = ({ @@ -20,17 +23,23 @@ export const ProSettings: React.FC = ({ delayMode, appliedDelayMs, isCapturing, + signalGeneratorConfig, + selectedDeviceId: propSelectedDeviceId, + onDeviceSelect, }) => { - const [selectedDeviceId, setSelectedDeviceId] = useState(""); - const [refChan, setRefChan] = useState(1); + const [localSelectedDeviceId, setLocalSelectedDeviceId] = useState(""); + const selectedDeviceId = propSelectedDeviceId || localSelectedDeviceId; + const [refChan, setRefChan] = useState(1); const [measChan, setMeasChan] = useState(2); const [nfft, setNfft] = useState(8192); + const [useLoopback, setUseLoopback] = useState(false); useEffect(() => { - if (devices.length > 0 && !selectedDeviceId) { - setSelectedDeviceId(devices[0].id); + if (devices.length > 0 && !localSelectedDeviceId && !propSelectedDeviceId) { + setLocalSelectedDeviceId(devices[0].id); + onDeviceSelect?.(devices[0].id); } - }, [devices, selectedDeviceId]); + }, [devices, localSelectedDeviceId, propSelectedDeviceId, onDeviceSelect]); const selectedDevice = devices.find((d) => d.id === selectedDeviceId); @@ -43,7 +52,7 @@ export const ProSettings: React.FC = ({ deviceId: selectedDeviceId, sampleRate: 48000, blockSize: 1024, - refChan, + refChan: useLoopback ? 1 : (refChan as number), // Use channel 1 for loopback internally measChan, nfft, avg: "power", @@ -51,6 +60,8 @@ export const ProSettings: React.FC = ({ window: "hann", lpfMode: "none", lpfFreq: 0, + useLoopback, + generator: signalGeneratorConfig, }; onStartCapture(config); }; @@ -84,7 +95,11 @@ export const ProSettings: React.FC = ({ setRefChan(Number(e.target.value))} + value={useLoopback ? "loopback" : refChan} + onChange={(e) => { + if (e.target.value === "loopback") { + setUseLoopback(true); + setRefChan(1); // Keep a default channel for internal use + } else { + setUseLoopback(false); + setRefChan(Number(e.target.value)); + } + }} disabled={isCapturing || !selectedDevice} className="w-full px-3 py-2 bg-gray-600 border border-gray-500 rounded-lg text-white disabled:opacity-50" > + {selectedDevice && renderChannelOptions(selectedDevice.inputs)} diff --git a/apps/web/src/components/analyzer/SignalGeneratorSettings.tsx b/apps/web/src/components/analyzer/SignalGeneratorSettings.tsx new file mode 100644 index 0000000..17fbf54 --- /dev/null +++ b/apps/web/src/components/analyzer/SignalGeneratorSettings.tsx @@ -0,0 +1,373 @@ +import React, { useState, useEffect } from "react"; +import { Volume2, Waves, Activity, TrendingUp, ChevronRight } from "lucide-react"; + +export interface SignalGeneratorConfig { + enabled: boolean; + signalType: "sine" | "white" | "pink" | "brown" | "blue" | "violet" | "sine_sweep"; + outputChannels: number[] | null; + frequency: number; + startFreq: number; + endFreq: number; + sweepDuration: number; + amplitude: number; +} + +interface SignalGeneratorSettingsProps { + deviceChannels: number; + onConfigChange: (config: SignalGeneratorConfig) => void; + isCapturing: boolean; +} + +const SignalGeneratorSettings: React.FC = ({ + deviceChannels, + onConfigChange, + isCapturing, +}) => { + const [config, setConfig] = useState({ + enabled: false, + signalType: "sine", + outputChannels: null, // null means all channels + frequency: 1000, + startFreq: 20, + endFreq: 20000, + sweepDuration: 1, + amplitude: 0.5, + }); + + const [selectedChannels, setSelectedChannels] = useState>(new Set()); + const [useAllChannels, setUseAllChannels] = useState(true); + + useEffect(() => { + onConfigChange({ + ...config, + outputChannels: useAllChannels ? null : Array.from(selectedChannels), + }); + }, [config, selectedChannels, useAllChannels]); + + const handleSignalTypeChange = (type: SignalGeneratorConfig["signalType"]) => { + setConfig((prev) => ({ ...prev, signalType: type })); + }; + + const handleToggleChannel = (channel: number) => { + setSelectedChannels((prev) => { + const newSet = new Set(prev); + if (newSet.has(channel)) { + newSet.delete(channel); + } else { + newSet.add(channel); + } + return newSet; + }); + }; + + const handleToggleAllChannels = () => { + if (useAllChannels) { + // Switch to individual selection, select first channel by default + setUseAllChannels(false); + setSelectedChannels(new Set([1])); + } else { + setUseAllChannels(true); + setSelectedChannels(new Set()); + } + }; + + const signalTypes = [ + { value: "sine", label: "Sine", icon: Waves }, + { value: "sine_sweep", label: "Sweep", icon: TrendingUp }, + { value: "white", label: "White", icon: Activity }, + { value: "pink", label: "Pink", icon: Activity }, + { value: "brown", label: "Brown", icon: Activity }, + { value: "blue", label: "Blue", icon: Activity }, + { value: "violet", label: "Violet", icon: Activity }, + ]; + + return ( +
+ {/* Header */} +
+
+ +

Signal Generator

+
+ + {/* Power Toggle */} + +
+ + {config.enabled && ( +
+ {/* Signal Type Grid */} +
+ +
+ {signalTypes.map((type) => { + const Icon = type.icon; + const isSelected = config.signalType === type.value; + return ( + + ); + })} +
+
+ + {/* Dynamic Controls Based on Signal Type */} +
+ {config.signalType === "sine" && ( +
+ +
+ { + // Convert frequency to logarithmic scale (0-100) + const minLog = Math.log10(20); + const maxLog = Math.log10(20000); + const scale = (Math.log10(config.frequency) - minLog) / (maxLog - minLog); + return scale * 100; + })()} + onChange={(e) => { + // Convert logarithmic scale back to frequency + const value = Number(e.target.value); + const minLog = Math.log10(20); + const maxLog = Math.log10(20000); + const frequency = Math.pow(10, minLog + (value / 100) * (maxLog - minLog)); + setConfig((prev) => ({ ...prev, frequency: Math.round(frequency) })); + }} + disabled={isCapturing} + className="flex-1 h-2 bg-gray-600 rounded appearance-none cursor-pointer disabled:opacity-50" + /> +
+ + setConfig((prev) => ({ ...prev, frequency: Number(e.target.value) })) + } + disabled={isCapturing} + className="w-full px-2 py-1 bg-gray-600 border border-gray-500 rounded text-white text-sm disabled:opacity-50 focus:border-indigo-500 focus:outline-none" + /> +
+ Hz +
+
+ 20 Hz + 200 Hz + 2 kHz + 20 kHz +
+
+ )} + + {config.signalType === "sine_sweep" && ( +
+
+
+ +
+ + setConfig((prev) => ({ ...prev, startFreq: Number(e.target.value) })) + } + disabled={isCapturing} + className="w-full px-2 py-1.5 bg-gray-600 border border-gray-500 rounded text-white text-sm disabled:opacity-50 focus:border-indigo-500 focus:outline-none" + /> + Hz +
+
+
+ +
+ + setConfig((prev) => ({ ...prev, endFreq: Number(e.target.value) })) + } + disabled={isCapturing} + className="w-full px-2 py-1.5 bg-gray-600 border border-gray-500 rounded text-white text-sm disabled:opacity-50 focus:border-indigo-500 focus:outline-none" + /> + Hz +
+
+
+ +
+ + setConfig((prev) => ({ ...prev, sweepDuration: Number(e.target.value) })) + } + disabled={isCapturing} + className="w-full px-2 py-1.5 bg-gray-600 border border-gray-500 rounded text-white text-sm disabled:opacity-50 focus:border-indigo-500 focus:outline-none" + /> + sec +
+
+
+
+ Logarithmic sweep: {config.startFreq} Hz → {config.endFreq} Hz ( + {config.sweepDuration}s) +
+
+ )} + + {!["sine", "sine_sweep"].includes(config.signalType) && ( +
+

+ {config.signalType === "white" && "Equal power at all frequencies"} + {config.signalType === "pink" && "1/f spectrum, equal power per octave"} + {config.signalType === "brown" && "1/f² spectrum, -6 dB per octave"} + {config.signalType === "blue" && "f spectrum, +3 dB per octave"} + {config.signalType === "violet" && "f² spectrum, +6 dB per octave"} +

+
+ )} +
+ + {/* Amplitude Control */} +
+
+ + {Math.round(config.amplitude * 100)}% +
+ + setConfig((prev) => ({ ...prev, amplitude: Number(e.target.value) / 100 })) + } + disabled={isCapturing} + className="w-full h-2 bg-gray-600 rounded appearance-none cursor-pointer disabled:opacity-50" + /> +
+ Silent + Full Scale +
+
+ + {/* Output Channel Selection */} +
+
+ + +
+ + {!useAllChannels && deviceChannels > 0 && ( +
+
+ {Array.from({ length: deviceChannels }, (_, i) => i + 1).map((channel) => ( + + ))} +
+ {selectedChannels.size === 0 && ( +

+ Select at least one channel +

+ )} +
+ )} +
+ + {/* Status Indicator */} + {isCapturing && config.enabled && ( +
+
+ Signal Active +
+ )} +
+ )} +
+ ); +}; + +export default SignalGeneratorSettings; diff --git a/apps/web/src/pages/AnalyzerProPage.tsx b/apps/web/src/pages/AnalyzerProPage.tsx index d3ad416..a4e18c7 100644 --- a/apps/web/src/pages/AnalyzerProPage.tsx +++ b/apps/web/src/pages/AnalyzerProPage.tsx @@ -7,6 +7,9 @@ import AgentConnectionManager from "../components/analyzer/AgentConnectionManage import AgentDownload from "../components/analyzer/AgentDownload"; import AgentUpdateNotification from "../components/analyzer/AgentUpdateNotification"; import ProSettings from "../components/analyzer/ProSettings"; +import SignalGeneratorSettings, { + SignalGeneratorConfig, +} from "../components/analyzer/SignalGeneratorSettings"; import SavedMeasurementsModal from "../components/analyzer/SavedMeasurementsModal"; import ChartDetailModal from "../components/analyzer/ChartDetailModal"; import MathTraceModal from "../components/analyzer/MathTraceModal"; @@ -50,7 +53,28 @@ const AnalyzerProPage: React.FC = () => { }>({}); const [agentVersion, setAgentVersion] = useState(); const [mathTraces, setMathTraces] = useState([]); + const [signalGeneratorConfig, setSignalGeneratorConfig] = useState({ + enabled: false, + signalType: "sine", + outputChannels: null, + frequency: 1000, + startFreq: 20, + endFreq: 20000, + sweepDuration: 1, + amplitude: 0.5, + }); const [computedMathTraces, setComputedMathTraces] = useState([]); + const [selectedDeviceId, setSelectedDeviceId] = useState(""); + + const handleGeneratorConfigChange = (config: SignalGeneratorConfig) => { + setSignalGeneratorConfig(config); + if (status === "connected") { + sendMessage({ + type: "update_generator", + config, + }); + } + }; const handleEqChange = async (id: string, eq_settings: EqSetting[]) => { // Update local state immediately for responsiveness @@ -176,6 +200,8 @@ const AnalyzerProPage: React.FC = () => { setComputedMathTraces(computed.filter((t): t is Measurement => t !== null)); }, [mathTraces, savedMeasurements]); + const selectedDevice = devices.find((d) => d.id === selectedDeviceId) || devices[0]; + useEffect(() => { if (status === "connected") { sendMessage({ type: "list_devices" }); @@ -188,6 +214,10 @@ const AnalyzerProPage: React.FC = () => { setAgentVersion(lastMessage.version); } else if (lastMessage.type === "devices") { setDevices(lastMessage.items); + // Set default selected device if none selected + if (!selectedDeviceId && lastMessage.items.length > 0) { + setSelectedDeviceId(lastMessage.items[0].id); + } } else if (lastMessage.type === "frame") { setIsCapturing(true); setTfData(lastMessage.tf); @@ -206,7 +236,7 @@ const AnalyzerProPage: React.FC = () => { } } } - }, [lastMessage]); + }, [lastMessage, selectedDeviceId]); const fetchMeasurements = async () => { try { @@ -446,6 +476,16 @@ const AnalyzerProPage: React.FC = () => { delayMode={delayMode} appliedDelayMs={appliedDelayMs} isCapturing={isCapturing} + signalGeneratorConfig={signalGeneratorConfig} + selectedDeviceId={selectedDeviceId} + onDeviceSelect={setSelectedDeviceId} + /> + )} + {status === "connected" && selectedDevice && selectedDevice.outputs > 0 && ( + )}
diff --git a/packages/analyzer-protocol/src/index.ts b/packages/analyzer-protocol/src/index.ts index 2b39bf7..018e9b5 100644 --- a/packages/analyzer-protocol/src/index.ts +++ b/packages/analyzer-protocol/src/index.ts @@ -25,6 +25,17 @@ export type WindowType = "hann" | "kaiser" | "blackman"; export type AvgType = "power" | "linear" | "exp"; export type LpfMode = "lpf" | "none"; +export interface SignalGeneratorConfig { + enabled: boolean; + signalType: "sine" | "white" | "pink" | "brown" | "blue" | "violet" | "sine_sweep"; + outputChannels?: number[] | null; + frequency: number; + startFreq: number; + endFreq: number; + sweepDuration: number; + amplitude: number; +} + export interface CaptureConfig { deviceId: string; sampleRate: number; @@ -41,6 +52,10 @@ export interface CaptureConfig { // Smoothing lpfMode: LpfMode; lpfFreq: number; + + // Signal Generator & Loopback + useLoopback?: boolean; + generator?: SignalGeneratorConfig; } // Message types from client to agent @@ -78,6 +93,11 @@ export interface DelayFreezeMessage { applied_ms?: number; } +export interface UpdateGeneratorMessage { + type: "update_generator"; + config: SignalGeneratorConfig; +} + export type ClientMessage = | HelloMessage | ListDevicesMessage @@ -85,7 +105,8 @@ export type ClientMessage = | StopCaptureMessage | CalibrateMessage | GetVersionMessage - | DelayFreezeMessage; + | DelayFreezeMessage + | UpdateGeneratorMessage; // Message types from agent to client export interface HelloAckMessage { @@ -164,6 +185,7 @@ export function isClientMessage(msg: ProtocolMessage): msg is ClientMessage { "calibrate", "get_version", "delay_freeze", + "update_generator", ].includes(msg.type); } From e25ce6bae885994f5660836c2ffa470c2b169f1f Mon Sep 17 00:00:00 2001 From: cj-vana Date: Thu, 25 Sep 2025 09:07:54 -0600 Subject: [PATCH 2/3] fix mypy and ESLint errors. --- CHANGELOG.md | 1 - .../capture_agent/signal_generator.py | 29 ++++++++++++------- .../src/components/analyzer/ProSettings.tsx | 4 +-- .../analyzer/SignalGeneratorSettings.tsx | 4 +-- apps/web/src/pages/AnalyzerProPage.tsx | 14 ++++----- 5 files changed, 29 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d691dd..82683fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - White Noise Generator: Full-spectrum white noise for system calibration - Sine Wave Generator: Pure tone generation with adjustable frequency (20Hz - 20kHz) - Sine Sweep Generator: Logarithmic sweep for system testing and alignment - - Click Track Generator: Metronome/timing reference with adjustable BPM - Configurable output routing to any available audio channel - Real-time signal generation with minimal latency - WebSocket control interface for remote signal control from web UI diff --git a/agents/capture-agent-py/capture_agent/signal_generator.py b/agents/capture-agent-py/capture_agent/signal_generator.py index 377025a..a004ba1 100644 --- a/agents/capture-agent-py/capture_agent/signal_generator.py +++ b/agents/capture-agent-py/capture_agent/signal_generator.py @@ -22,7 +22,7 @@ class SignalType(Enum): class GeneratorConfig: signal_type: SignalType sample_rate: int = 48000 - output_channels: List[int] = None # Which channels to output to, None = all + output_channels: Optional[List[int]] = None # Which channels to output to, None = all # Sine specific frequency: float = 1000.0 # Hz # Sine sweep specific @@ -56,12 +56,12 @@ def __init__(self, config: GeneratorConfig): } # Pre-generated noise table for smooth playback - self._noise_table = None + self._noise_table: Optional[np.ndarray] = None self._noise_len = 0 self._noise_pos = 0 self._xfade_len = 2048 # ~43 ms @48kHz for smooth crossfade - self._xfade_in = None - self._xfade_out = None + self._xfade_in: Optional[np.ndarray] = None + self._xfade_out: Optional[np.ndarray] = None # Pre-generate noise table if needed if self.config.signal_type in self.noise_beta_map: @@ -217,7 +217,10 @@ def _generate_colored_noise(self, block_size: int) -> np.ndarray: if p + block_size <= N: # Simple case: read straight from table - out = self._noise_table[p:p + block_size].copy() + if self._noise_table is not None: + out = self._noise_table[p:p + block_size].copy() + else: + out = np.zeros(block_size, dtype=np.float32) p += block_size if p == N: p = 0 # Wrap to beginning @@ -226,16 +229,20 @@ def _generate_colored_noise(self, block_size: int) -> np.ndarray: n1 = N - p # Samples from end n2 = block_size - n1 # Samples from beginning out = np.empty(block_size, dtype=np.float32) - out[:n1] = self._noise_table[p:] - out[n1:] = self._noise_table[:n2] + if self._noise_table is not None: + out[:n1] = self._noise_table[p:] + out[n1:] = self._noise_table[:n2] # Apply crossfade at the loop seam for click-free playback if n1 >= L and n2 >= L: # Crossfade last L samples of part1 with first L samples of part2 - fade_out_portion = self._noise_table[N - L:N] * self._xfade_out - fade_in_portion = self._noise_table[:L] * self._xfade_in - # The crossfade happens at the boundary - crossfaded = fade_out_portion + fade_in_portion + if self._noise_table is not None and self._xfade_out is not None and self._xfade_in is not None: + fade_out_portion = self._noise_table[N - L:N] * self._xfade_out + fade_in_portion = self._noise_table[:L] * self._xfade_in + # The crossfade happens at the boundary + crossfaded = fade_out_portion + fade_in_portion + else: + crossfaded = np.zeros(L, dtype=np.float32) # Replace the boundary region with crossfaded version if n1 >= L: out[n1 - L:n1] = crossfaded diff --git a/apps/web/src/components/analyzer/ProSettings.tsx b/apps/web/src/components/analyzer/ProSettings.tsx index c988a1c..90f742b 100644 --- a/apps/web/src/components/analyzer/ProSettings.tsx +++ b/apps/web/src/components/analyzer/ProSettings.tsx @@ -10,7 +10,7 @@ interface ProSettingsProps { delayMode?: string; appliedDelayMs?: number; isCapturing: boolean; - signalGeneratorConfig?: any; + signalGeneratorConfig?: unknown; selectedDeviceId?: string; onDeviceSelect?: (deviceId: string) => void; } @@ -31,7 +31,7 @@ export const ProSettings: React.FC = ({ const selectedDeviceId = propSelectedDeviceId || localSelectedDeviceId; const [refChan, setRefChan] = useState(1); const [measChan, setMeasChan] = useState(2); - const [nfft, setNfft] = useState(8192); + const [nfft] = useState(8192); const [useLoopback, setUseLoopback] = useState(false); useEffect(() => { diff --git a/apps/web/src/components/analyzer/SignalGeneratorSettings.tsx b/apps/web/src/components/analyzer/SignalGeneratorSettings.tsx index 17fbf54..dcb4036 100644 --- a/apps/web/src/components/analyzer/SignalGeneratorSettings.tsx +++ b/apps/web/src/components/analyzer/SignalGeneratorSettings.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from "react"; -import { Volume2, Waves, Activity, TrendingUp, ChevronRight } from "lucide-react"; +import { Volume2, Waves, Activity, TrendingUp } from "lucide-react"; export interface SignalGeneratorConfig { enabled: boolean; @@ -42,7 +42,7 @@ const SignalGeneratorSettings: React.FC = ({ ...config, outputChannels: useAllChannels ? null : Array.from(selectedChannels), }); - }, [config, selectedChannels, useAllChannels]); + }, [config, selectedChannels, useAllChannels, onConfigChange]); const handleSignalTypeChange = (type: SignalGeneratorConfig["signalType"]) => { setConfig((prev) => ({ ...prev, signalType: type })); diff --git a/apps/web/src/pages/AnalyzerProPage.tsx b/apps/web/src/pages/AnalyzerProPage.tsx index a4e18c7..ea064cd 100644 --- a/apps/web/src/pages/AnalyzerProPage.tsx +++ b/apps/web/src/pages/AnalyzerProPage.tsx @@ -17,7 +17,7 @@ import { TransferFunctionVisualizer } from "@sounddocs/analyzer-lite"; import { useCaptureAgent } from "../stores/agentStore"; import { supabase } from "../lib/supabase"; import { Device, TFData } from "@sounddocs/analyzer-protocol"; -import { useState, useEffect, useMemo } from "react"; +import { useState, useEffect, useCallback } from "react"; import { TRACE_COLORS } from "../lib/constants"; import { EqSetting, calculateMathTrace } from "../lib/dsp"; import { Measurement } from "../lib/types"; @@ -163,10 +163,6 @@ const AnalyzerProPage: React.FC = () => { } }; - useEffect(() => { - fetchMeasurements(); - }, []); - useEffect(() => { const measurementsById = new Map(savedMeasurements.map((m) => [m.id, m])); const computed = mathTraces.map((trace): Measurement | null => { @@ -238,7 +234,7 @@ const AnalyzerProPage: React.FC = () => { } }, [lastMessage, selectedDeviceId]); - const fetchMeasurements = async () => { + const fetchMeasurements = useCallback(async () => { try { const { data, error } = await supabase .from("tf_measurements") @@ -278,7 +274,11 @@ const AnalyzerProPage: React.FC = () => { } catch (error) { console.error("Error fetching measurements:", error); } - }; + }, [measurementAdjustments]); + + useEffect(() => { + fetchMeasurements(); + }, [fetchMeasurements]); const handleDeleteMeasurement = async (id: string) => { try { From 6f0e9bff7ca5464feda2997550f9fd782de50317 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Thu, 25 Sep 2025 09:19:49 -0600 Subject: [PATCH 3/3] Revert "fix mypy and ESLint errors." This reverts commit e25ce6bae885994f5660836c2ffa470c2b169f1f. --- CHANGELOG.md | 1 + .../capture_agent/signal_generator.py | 29 +++++++------------ .../src/components/analyzer/ProSettings.tsx | 4 +-- .../analyzer/SignalGeneratorSettings.tsx | 4 +-- apps/web/src/pages/AnalyzerProPage.tsx | 14 ++++----- 5 files changed, 23 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82683fd..7d691dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - White Noise Generator: Full-spectrum white noise for system calibration - Sine Wave Generator: Pure tone generation with adjustable frequency (20Hz - 20kHz) - Sine Sweep Generator: Logarithmic sweep for system testing and alignment + - Click Track Generator: Metronome/timing reference with adjustable BPM - Configurable output routing to any available audio channel - Real-time signal generation with minimal latency - WebSocket control interface for remote signal control from web UI diff --git a/agents/capture-agent-py/capture_agent/signal_generator.py b/agents/capture-agent-py/capture_agent/signal_generator.py index a004ba1..377025a 100644 --- a/agents/capture-agent-py/capture_agent/signal_generator.py +++ b/agents/capture-agent-py/capture_agent/signal_generator.py @@ -22,7 +22,7 @@ class SignalType(Enum): class GeneratorConfig: signal_type: SignalType sample_rate: int = 48000 - output_channels: Optional[List[int]] = None # Which channels to output to, None = all + output_channels: List[int] = None # Which channels to output to, None = all # Sine specific frequency: float = 1000.0 # Hz # Sine sweep specific @@ -56,12 +56,12 @@ def __init__(self, config: GeneratorConfig): } # Pre-generated noise table for smooth playback - self._noise_table: Optional[np.ndarray] = None + self._noise_table = None self._noise_len = 0 self._noise_pos = 0 self._xfade_len = 2048 # ~43 ms @48kHz for smooth crossfade - self._xfade_in: Optional[np.ndarray] = None - self._xfade_out: Optional[np.ndarray] = None + self._xfade_in = None + self._xfade_out = None # Pre-generate noise table if needed if self.config.signal_type in self.noise_beta_map: @@ -217,10 +217,7 @@ def _generate_colored_noise(self, block_size: int) -> np.ndarray: if p + block_size <= N: # Simple case: read straight from table - if self._noise_table is not None: - out = self._noise_table[p:p + block_size].copy() - else: - out = np.zeros(block_size, dtype=np.float32) + out = self._noise_table[p:p + block_size].copy() p += block_size if p == N: p = 0 # Wrap to beginning @@ -229,20 +226,16 @@ def _generate_colored_noise(self, block_size: int) -> np.ndarray: n1 = N - p # Samples from end n2 = block_size - n1 # Samples from beginning out = np.empty(block_size, dtype=np.float32) - if self._noise_table is not None: - out[:n1] = self._noise_table[p:] - out[n1:] = self._noise_table[:n2] + out[:n1] = self._noise_table[p:] + out[n1:] = self._noise_table[:n2] # Apply crossfade at the loop seam for click-free playback if n1 >= L and n2 >= L: # Crossfade last L samples of part1 with first L samples of part2 - if self._noise_table is not None and self._xfade_out is not None and self._xfade_in is not None: - fade_out_portion = self._noise_table[N - L:N] * self._xfade_out - fade_in_portion = self._noise_table[:L] * self._xfade_in - # The crossfade happens at the boundary - crossfaded = fade_out_portion + fade_in_portion - else: - crossfaded = np.zeros(L, dtype=np.float32) + fade_out_portion = self._noise_table[N - L:N] * self._xfade_out + fade_in_portion = self._noise_table[:L] * self._xfade_in + # The crossfade happens at the boundary + crossfaded = fade_out_portion + fade_in_portion # Replace the boundary region with crossfaded version if n1 >= L: out[n1 - L:n1] = crossfaded diff --git a/apps/web/src/components/analyzer/ProSettings.tsx b/apps/web/src/components/analyzer/ProSettings.tsx index 90f742b..c988a1c 100644 --- a/apps/web/src/components/analyzer/ProSettings.tsx +++ b/apps/web/src/components/analyzer/ProSettings.tsx @@ -10,7 +10,7 @@ interface ProSettingsProps { delayMode?: string; appliedDelayMs?: number; isCapturing: boolean; - signalGeneratorConfig?: unknown; + signalGeneratorConfig?: any; selectedDeviceId?: string; onDeviceSelect?: (deviceId: string) => void; } @@ -31,7 +31,7 @@ export const ProSettings: React.FC = ({ const selectedDeviceId = propSelectedDeviceId || localSelectedDeviceId; const [refChan, setRefChan] = useState(1); const [measChan, setMeasChan] = useState(2); - const [nfft] = useState(8192); + const [nfft, setNfft] = useState(8192); const [useLoopback, setUseLoopback] = useState(false); useEffect(() => { diff --git a/apps/web/src/components/analyzer/SignalGeneratorSettings.tsx b/apps/web/src/components/analyzer/SignalGeneratorSettings.tsx index dcb4036..17fbf54 100644 --- a/apps/web/src/components/analyzer/SignalGeneratorSettings.tsx +++ b/apps/web/src/components/analyzer/SignalGeneratorSettings.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from "react"; -import { Volume2, Waves, Activity, TrendingUp } from "lucide-react"; +import { Volume2, Waves, Activity, TrendingUp, ChevronRight } from "lucide-react"; export interface SignalGeneratorConfig { enabled: boolean; @@ -42,7 +42,7 @@ const SignalGeneratorSettings: React.FC = ({ ...config, outputChannels: useAllChannels ? null : Array.from(selectedChannels), }); - }, [config, selectedChannels, useAllChannels, onConfigChange]); + }, [config, selectedChannels, useAllChannels]); const handleSignalTypeChange = (type: SignalGeneratorConfig["signalType"]) => { setConfig((prev) => ({ ...prev, signalType: type })); diff --git a/apps/web/src/pages/AnalyzerProPage.tsx b/apps/web/src/pages/AnalyzerProPage.tsx index ea064cd..a4e18c7 100644 --- a/apps/web/src/pages/AnalyzerProPage.tsx +++ b/apps/web/src/pages/AnalyzerProPage.tsx @@ -17,7 +17,7 @@ import { TransferFunctionVisualizer } from "@sounddocs/analyzer-lite"; import { useCaptureAgent } from "../stores/agentStore"; import { supabase } from "../lib/supabase"; import { Device, TFData } from "@sounddocs/analyzer-protocol"; -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useMemo } from "react"; import { TRACE_COLORS } from "../lib/constants"; import { EqSetting, calculateMathTrace } from "../lib/dsp"; import { Measurement } from "../lib/types"; @@ -163,6 +163,10 @@ const AnalyzerProPage: React.FC = () => { } }; + useEffect(() => { + fetchMeasurements(); + }, []); + useEffect(() => { const measurementsById = new Map(savedMeasurements.map((m) => [m.id, m])); const computed = mathTraces.map((trace): Measurement | null => { @@ -234,7 +238,7 @@ const AnalyzerProPage: React.FC = () => { } }, [lastMessage, selectedDeviceId]); - const fetchMeasurements = useCallback(async () => { + const fetchMeasurements = async () => { try { const { data, error } = await supabase .from("tf_measurements") @@ -274,11 +278,7 @@ const AnalyzerProPage: React.FC = () => { } catch (error) { console.error("Error fetching measurements:", error); } - }, [measurementAdjustments]); - - useEffect(() => { - fetchMeasurements(); - }, [fetchMeasurements]); + }; const handleDeleteMeasurement = async (id: string) => { try {