[Image: banniereforumhifi.jpg] (September 11) x

Note de ce sujet :
  • Moyenne : 5 (1 vote(s))
  • 1
  • 2
  • 3
  • 4
  • 5
Installation de HQPe/NAA/Diretta ALSA drivers sous Fedora
#30
Pour vous permettre de faire vos premiers pas dans l'optimisation de Diretta en mode DDS, voici un programme python qui calcule une proposition de fichier de paramètres adaptés. N'oubliez pas de démarrer à très bas volume pour ne pas griller vos enceintes. Je dégage toute responsabilité...


Code :
#!/usr/bin/env python3
"""
Diretta DDS Configuration Calculator
Based on real working configuration with adaptive CycleTime
Optimized settings: periodMin=4, periodMax=8, syncBufferCount=6

Usage:
    python3 diretta_calc.py                    # Default optimized settings
    python3 diretta_calc.py --period-max 6    # Lower latency
    python3 diretta_calc.py --pcm              # PCM instead of DSD
    python3 diretta_calc.py --help            # Show all options
"""

import argparse
from typing import Dict, List, Tuple

class DirettaCalculator:
    """Calculate Diretta configuration parameters"""
   
    # Ethernet frame overhead
    ETH_HEADER = 14
    DIRETTA_HEADER = 2
    VLAN_OVERHEAD = 4
    FCS = 4
   
    def __init__(self, mtu: int = 9024):
        self.mtu = mtu
        self.overhead = self.ETH_HEADER + self.DIRETTA_HEADER + self.VLAN_OVERHEAD
        self.available_payload = mtu - self.overhead
       
    def calculate_frame_params(self, sample_rate: int, is_dsd: bool = True) -> Dict:
        """
        Calculate frame parameters for a given sample rate
       
        Args:
            sample_rate: Sample rate in Hz (e.g., 12288000 for DSD256×48k)
            is_dsd: True for DSD, False for PCM
       
        Returns:
            Dictionary with frame parameters
        """
        # Bytes per sample (stereo)
        if is_dsd:
            bytes_per_sample = 0.25  # 1 bit/sample × 2 channels = 2 bits = 0.25 bytes
        else:
            bytes_per_sample = 6  # 24 bit/sample × 2 channels = 48 bits = 6 bytes
       
        # Calculate samples that fit in available payload
        # Align to 64-bit (8 byte) boundary
        audio_bytes_max = (int(self.available_payload / 8) * 8)
        samples_per_frame = int(audio_bytes_max / bytes_per_sample)
       
        # Actual audio bytes after alignment
        audio_bytes = int(samples_per_frame * bytes_per_sample)
       
        # Calculate cycle time (time to transmit one frame)
        cycle_time_us = (samples_per_frame / sample_rate) * 1_000_000
       
        # Packet rate (packets per second)
        packet_rate_hz = sample_rate / samples_per_frame
       
        # Total frame size
        total_frame_size = self.ETH_HEADER + self.DIRETTA_HEADER + audio_bytes + self.FCS
       
        return {
            'sample_rate': sample_rate,
            'sample_rate_mhz': sample_rate / 1_000_000,
            'samples_per_frame': samples_per_frame,
            'audio_bytes': audio_bytes,
            'total_frame_size': total_frame_size,
            'cycle_time_us': cycle_time_us,
            'cycle_time_ms': cycle_time_us / 1000,
            'packet_rate_hz': packet_rate_hz,
            'bytes_per_sample': bytes_per_sample,
            'is_dsd': is_dsd,
        }
   
    def find_target_cycle_time(self, base_rates: List[int], is_dsd: bool = True) -> Tuple[int, Dict]:
        """
        Find a target CycleTime that works well across multiple base rates
       
        Args:
            base_rates: List of base sample rates (e.g., [44100, 48000])
            is_dsd: True for DSD, False for PCM
       
        Returns:
            Tuple of (recommended_cycle_time_us, analysis_dict)
        """
        results = {}
       
        for base_rate in base_rates:
            # For DSD256, multiply by 256; for PCM 8fs, multiply by 8
            multiplier = 256 if is_dsd else 8
            sample_rate = base_rate * multiplier
           
            params = self.calculate_frame_params(sample_rate, is_dsd)
            results[base_rate] = params
       
        # Find the cycle time that's closest to average
        cycle_times = [p['cycle_time_us'] for p in results.values()]
        avg_cycle = sum(cycle_times) / len(cycle_times)
       
        # Round to nearest 5 microseconds for cleaner config
        target_cycle = int(round(avg_cycle / 5) * 5)
       
        return target_cycle, results


def print_header():
    """Print program header"""
    print("""
╔═══════════════════════════════════════════════════════════════════════════════╗
║                  DIRETTA DDS CONFIGURATION CALCULATOR                        ║
║                        Low Latency Optimizer                                ║
╚═══════════════════════════════════════════════════════════════════════════════╝
""")


def print_analysis_table(calculator: DirettaCalculator, configs: List[Dict]):
    """Print analysis table for different sample rates"""
   
    print(f"\n{'='*100}")
    print(f"SAMPLE RATE ANALYSIS (MTU = {calculator.mtu} bytes)")
    print(f"{'='*100}")
   
    print(f"\n{'Format':<20} {'Sample Rate':<15} {'Samples/':<12} {'Cycle':<12} {'Packet':<12} {'Audio':<10}")
    print(f"{'':20} {'(MHz)':<15} {'Frame':<12} {'(μs)':<12} {'Rate (Hz)':<12} {'Bytes':<10}")
    print("-" * 100)
   
    for config in configs:
        params = calculator.calculate_frame_params(config['sample_rate'], config['is_dsd'])
       
        print(f"{config['name']:<20} "
              f"{params['sample_rate_mhz']:>8.6f}      "
              f"{params['samples_per_frame']:>8}    "
              f"{params['cycle_time_us']:>8.1f}    "
              f"{params['packet_rate_hz']:>8.2f}    "
              f"{params['audio_bytes']:>6}")


def print_latency_table(calculator: DirettaCalculator, configs: List[Dict],
                      period_min: int, period_max: int, sync_buffer: int):
    """Print detailed latency table for all configurations"""
   
    print(f"\n{'='*100}")
    print(f"LATENCY COMPARISON TABLE")
    print(f"{'='*100}")
   
    buffer_configs = [
        ("Ultra Conservative", 6, 12, 10),
        ("Conservative", 5, 10, 8),
        ("Optimized (recommended)", 4, 8, 6),
        ("Lower Latency", 3, 6, 5),
        ("Aggressive", 3, 6, 4),
    ]
   
    # Highlight the current settings
    current_config = (f"Your Settings", period_min, period_max, sync_buffer)
   
    print(f"\n{'Configuration':<25} {'Periods':<12} {'Sync':<8} {'48kHz':<12} {'44.1kHz':<12} {'Notes'}")
    print(f"{'':25} {'(min/max)':<12} {'Buf':<8} {'Latency':<12} {'Latency':<12}")
    print("-" * 100)
   
    # Find DSD256 configs for latency calculation
    dsd256_48k = None
    dsd256_441k = None
    for config in configs:
        if config['sample_rate'] == 12288000:
            dsd256_48k = calculator.calculate_frame_params(config['sample_rate'], True)
        elif config['sample_rate'] == 11289600:
            dsd256_441k = calculator.calculate_frame_params(config['sample_rate'], True)
   
    for name, pmin, pmax, sync in buffer_configs:
        lat_48k = dsd256_48k['cycle_time_ms'] * pmax if dsd256_48k else 0
        lat_441k = dsd256_441k['cycle_time_ms'] * pmax if dsd256_441k else 0
       
        is_current = (pmin == period_min and pmax == period_max and sync == sync_buffer)
        marker = ">>> " if is_current else "    "
       
        notes = ""
        if pmax >= 10:
            notes = "Very stable"
        elif pmax == 8:
            notes = "Good balance"
        elif pmax == 6:
            notes = "Monitor closely"
        else:
            notes = "Risky!"
       
        print(f"{marker}{name:<21} "
              f"{pmin:>3}/{pmax:<7} "
              f"{sync:>4}    "
              f"{lat_48k:>7.2f} ms  "
              f"{lat_441k:>7.2f} ms  "
              f"{notes}")
   
    # Add your current settings if they're custom
    if not any(pmin == period_min and pmax == period_max and sync == sync_buffer
              for _, pmin, pmax, sync in buffer_configs):
        lat_48k = dsd256_48k['cycle_time_ms'] * period_max if dsd256_48k else 0
        lat_441k = dsd256_441k['cycle_time_ms'] * period_max if dsd256_441k else 0
       
        print(f">>> Your Settings        "
              f"{period_min:>3}/{period_max:<7} "
              f"{sync_buffer:>4}    "
              f"{lat_48k:>7.2f} ms  "
              f"{lat_441k:>7.2f} ms  "
              f"Custom")


def print_improvement_comparison():
    """Show improvement from conservative to optimized settings"""
    print(f"\n{'='*100}")
    print("LATENCY IMPROVEMENT: Conservative → Optimized")
    print(f"{'='*100}")
   
    print("\nConservative Settings (periodMin=5, periodMax=10, syncBufferCount=8):")
    print("  48kHz family:  ~29.4 ms")
    print("  44.1kHz family: ~32.0 ms")
   
    print("\nOptimized Settings (periodMin=4, periodMax=8, syncBufferCount=6):")
    print("  48kHz family:  ~23.5 ms  (↓ 5.9 ms, 20% faster)")
    print("  44.1kHz family: ~25.6 ms  (↓ 6.4 ms, 20% faster)")
   
    print("\nWhat Changed:")
    print("  • periodMax:      10 → 8  (2 fewer periods)")
    print("  • periodMin:        5 → 4  (tighter ratio)")
    print("  • syncBufferCount:  8 → 6  (2 fewer buffers)")
    print("  • CycleTime:    2935 μs    (unchanged - proven stable)")
   
    print("\nStability Analysis:")
    print("  ✓ periodMin/Max ratio: 4:8 = 1:2  (good balance)")
    print("  ✓ Total buffering: 8 periods (reasonable)")
    print("  ✓ Sync buffer: 6 (adequate for clock drift)")
    print("  ⚠ Monitor for underruns on congested networks")


def generate_config(cycle_time: int, period_min: int = 4, period_max: int = 8,
                  period_size_min: int = 2048, period_size_max: int = 8192,
                  sync_buffer: int = 6, latency_buffer: int = 0,
                  thred_mode: int = 257, interface: str = 'enp5s0',
                  cpu_send: int = 1, cpu_other: int = 2,
                  margin_us: int = 20) -> str:
    """Generate Diretta configuration file content"""
   
    cycle_min = cycle_time - margin_us
   
    config = f"""[global]
Interface={interface}
TargetProfileLimitTime=0
ThredMode={thred_mode}
InfoCycle=100000
FlexCycle=max
CycleTime={cycle_time}
CycleMinTime={cycle_min}
Debug=stdout
periodMax={period_max}
periodMin={period_min}
periodSizeMax={period_size_max}
periodSizeMin={period_size_min}
syncBufferCount={sync_buffer}
alsaUnderrun=enable
unInitMemDet=disable
CpuSend={cpu_send}
CpuOther={cpu_other}
LatencyBuffer={latency_buffer}
"""
    return config


def print_usage_guide():
    """Print usage and monitoring guide"""
    print(f"\n{'='*100}")
    print("USAGE GUIDE")
    print(f"{'='*100}")
   
    print("""
1. SAVE CONFIGURATION
  sudo nano ~/DirettaAlsaHost/setting.inf
  (paste the configuration shown above)

2. RESTART DIRETTA
  sudo systemctl restart diretta_sync_host.service

3. MONITOR OPERATION
  journalctl -u diretta_sync_host -f
 
  Look for these indicators:
  ✓ "start cycle=XXXXusec" - shows actual cycle time
  ✓ "info rcv" - timing statistics (should be stable)
  ✗ "Buffer underrun" - indicates buffers too small
  ✗ Timing jitter >100μs - network congestion

4. VERIFY EXPECTED BEHAVIOR
  • CycleTime will auto-adjust per sample rate
  • 48kHz: ~2938 μs (340 packets/sec)
  • 44.1kHz: ~3198 μs (313 packets/sec)
  • This is normal and expected!

5. TROUBLESHOOTING
  Buffer underruns occur:
    → Increase periodMax: --period-max 10
    → Increase syncBuffer: --sync-buffer 8
 
  Want lower latency:
    → Decrease periodMax: --period-max 6
    → Decrease syncBuffer: --sync-buffer 5
    → Monitor closely for underruns!

6. NETWORK OPTIMIZATION
  • Ensure jumbo frames enabled: ip link set enp5s0 mtu 9024
  • Disable offloading: ethtool -K enp5s0 gro off lro off
  • Check no packet loss: ethtool -S enp5s0 | grep errors
  • Isolate Diretta traffic on dedicated network
""")


def print_key_concepts():
    """Explain key concepts"""
    print(f"\n{'='*100}")
    print("KEY CONCEPTS EXPLAINED")
    print(f"{'='*100}")
   
    print("""
CYCLE TIME (Adaptive)
  • You configure: CycleTime=2935 (target value)
  • Diretta adjusts: Actual cycle varies by sample rate
  • Purpose: Maintains constant packet size (9024 bytes)
  • Why adaptive: Different sample rates need different timing

PERIOD BUFFERS
  • periodMin/Max: Range of buffering depth
  • Each period = one CycleTime
  • periodMax determines total latency
  • Example: 8 periods × 2.938 ms = 23.5 ms latency

SYNC BUFFER
  • syncBufferCount: Additional buffer for clock sync
  • Compensates for clock drift between sender/receiver
  • Too small: Sync issues, clicks/pops
  • Too large: Added latency

PACKET STRUCTURE
  • Total frame: 9024 bytes (jumbo frame)
  • Overhead: 20 bytes (ethernet + diretta headers)
  • Audio payload: ~9004 bytes
  • Samples per packet: Varies by sample rate

LATENCY CALCULATION
  • Total latency = CycleTime × periodMax
  • Does NOT include:
    - DAC processing time
    - Network propagation delay (usually <1ms)
    - USB/SPDIF converter latency
  • Actual end-to-end: Add ~2-5ms for complete chain
""")


def main():
    parser = argparse.ArgumentParser(
        description='Diretta DDS Configuration Calculator - Optimized Settings',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  %(prog)s                                  # Default optimized config
  %(prog)s --period-max 6 --sync-buffer 5  # Lower latency (experimental)
  %(prog)s --period-max 10 --sync-buffer 8 # More conservative
  %(prog)s --pcm                            # PCM instead of DSD
  %(prog)s --mtu 1500                      # Standard ethernet MTU
  %(prog)s --config-only                    # Just show config file
        """
    )
   
    parser.add_argument('--mtu', type=int, default=9024,
                      help='MTU size (default: 9024 for jumbo frames)')
    parser.add_argument('--pcm', action='store_true',
                      help='Calculate for PCM instead of DSD')
    parser.add_argument('--period-min', type=int, default=4,
                      help='periodMin value (default: 4)')
    parser.add_argument('--period-max', type=int, default=8,
                      help='periodMax value (default: 8)')
    parser.add_argument('--sync-buffer', type=int, default=6,
                      help='syncBufferCount value (default: 6)')
    parser.add_argument('--latency-buffer', type=int, default=0,
                      help='LatencyBuffer value (default: 0)')
    parser.add_argument('--period-size-min', type=int, default=2048,
                      help='periodSizeMin value (default: 2048)')
    parser.add_argument('--period-size-max', type=int, default=8192,
                      help='periodSizeMax value (default: 8192)')
    parser.add_argument('--config-only', action='store_true',
                      help='Only generate config, skip analysis')
    parser.add_argument('--thred-mode', type=int, default=257,
                      help='ThredMode value (default: 257)')
    parser.add_argument('--interface', type=str, default='enp5s0',
                      help='Network interface (default: enp5s0)')
    parser.add_argument('--cpu-send', type=int, default=1,
                      help='CPU core for send thread (default: 1)')
    parser.add_argument('--cpu-other', type=int, default=2,
                      help='CPU core for other threads (default: 2)')
   
    args = parser.parse_args()
   
    calculator = DirettaCalculator(mtu=args.mtu)
    is_dsd = not args.pcm
    format_type = "DSD" if is_dsd else "PCM"
   
    # Define sample rate configurations
    if is_dsd:
        configs = [
            {'name': 'DSD256 × 48kHz', 'sample_rate': 48000 * 256, 'is_dsd': True},
            {'name': 'DSD256 × 44.1kHz', 'sample_rate': 44100 * 256, 'is_dsd': True},
            {'name': 'DSD128 × 48kHz', 'sample_rate': 48000 * 128, 'is_dsd': True},
            {'name': 'DSD128 × 44.1kHz', 'sample_rate': 44100 * 128, 'is_dsd': True},
            {'name': 'DSD64 × 48kHz', 'sample_rate': 48000 * 64, 'is_dsd': True},
            {'name': 'DSD64 × 44.1kHz', 'sample_rate': 44100 * 64, 'is_dsd': True},
        ]
    else:
        configs = [
            {'name': 'PCM 384kHz (8×48k)', 'sample_rate': 48000 * 8, 'is_dsd': False},
            {'name': 'PCM 352.8kHz (8×44.1k)', 'sample_rate': 44100 * 8, 'is_dsd': False},
            {'name': 'PCM 192kHz (4×48k)', 'sample_rate': 48000 * 4, 'is_dsd': False},
            {'name': 'PCM 176.4kHz (4×44.1k)', 'sample_rate': 44100 * 4, 'is_dsd': False},
            {'name': 'PCM 96kHz (2×48k)', 'sample_rate': 48000 * 2, 'is_dsd': False},
            {'name': 'PCM 88.2kHz (2×44.1k)', 'sample_rate': 44100 * 2, 'is_dsd': False},
        ]
   
    # Find target cycle time
    base_rates = [44100, 48000]
    target_cycle_us, analysis = calculator.find_target_cycle_time(base_rates, is_dsd)
   
    # Print analysis unless config-only
    if not args.config_only:
        print_header()
       
        print(f"Format Type: {format_type}")
        print(f"MTU: {args.mtu} bytes")
        print(f"Target CycleTime: {target_cycle_us} μs")
        print(f"\nBuffer Configuration:")
        print(f"  periodMin: {args.period_min}")
        print(f"  periodMax: {args.period_max}")
        print(f"  syncBufferCount: {args.sync_buffer}")
       
        print_analysis_table(calculator, configs)
        print_latency_table(calculator, configs, args.period_min, args.period_max, args.sync_buffer)
       
        if args.period_min == 4 and args.period_max == 8 and args.sync_buffer == 6:
            print_improvement_comparison()
       
        print_key_concepts()
   
    # Generate and display configuration
    print(f"\n{'='*100}")
    print(f"CONFIGURATION FILE ({format_type} mode)")
    print(f"{'='*100}")
   
    # Calculate expected latencies
    dsd256_48k = calculator.calculate_frame_params(12288000 if is_dsd else 384000, is_dsd)
    dsd256_441k = calculator.calculate_frame_params(11289600 if is_dsd else 352800, is_dsd)
   
    lat_48k = dsd256_48k['cycle_time_ms'] * args.period_max
    lat_441k = dsd256_441k['cycle_time_ms'] * args.period_max
   
    print(f"\nExpected Latency:")
    print(f"  48kHz family:  ~{lat_48k:.1f} ms")
    print(f"  44.1kHz family: ~{lat_441k:.1f} ms")
   
    config = generate_config(
        cycle_time=target_cycle_us,
        period_min=args.period_min,
        period_max=args.period_max,
        period_size_min=args.period_size_min,
        period_size_max=args.period_size_max,
        sync_buffer=args.sync_buffer,
        latency_buffer=args.latency_buffer,
        thred_mode=args.thred_mode,
        interface=args.interface,
        cpu_send=args.cpu_send,
        cpu_other=args.cpu_other,
    )
   
    print(config)
   
    if not args.config_only:
        print_usage_guide()
       
        print(f"\n{'='*100}")
        print("NEXT STEPS")
        print(f"{'='*100}")
        print(f"""
1. Copy the configuration above to ~/DirettaAlsaHost/setting.inf
2. Restart Diretta: sudo systemctl restart diretta_sync_host.service
3. Monitor: journalctl -u diretta_sync_host -f
4. Verify ~4ms latency achieved

For different configurations:
  • Lower latency:      python3 {__file__} --period-max 6 --sync-buffer 5
  • More conservative:  python3 {__file__} --period-max 10 --sync-buffer 8
  • Standard MTU:      python3 {__file__} --mtu 1500
  • PCM mode:          python3 {__file__} --pcm
""")


if __name__ == "__main__":
    main()


Messages dans ce sujet
RE: Installation de HQPe/NAA/Diretta ALSA drivers sous Fedora - par Bear - 10-31-2025, 11:15 PM

Sujets apparemment similaires…
Sujet Auteur Réponses Affichages Dernier message
  Carte Mère SOtM - sMB-Q370 pour TARGET DIRETTA jean-luc 122 64,532 12-04-2024, 12:20 PM
Dernier message: bbill
  Soucis Windows 7 sous Mac OS Euterpe 26 8,596 10-23-2024, 09:11 AM
Dernier message: Le Moine Bleu
  Diretta Lucia Development kit Olivier 131 79,877 07-15-2024, 10:12 PM
Dernier message: Le dom
  ENGINEERED fermé ou service client sous l'eau ? Moxa 15 8,108 01-27-2024, 02:26 PM
Dernier message: thomasv
Music Room Shaper sous Win avec Roon et HQPlayer zaurux 4 4,239 11-10-2023, 12:43 AM
Dernier message: ds21

Atteindre :


Utilisateur(s) parcourant ce sujet : 1 visiteur(s)