Invoke-DJScratchLooper.ps1

function Invoke-DJScratchLooper {
    <#
    .SYNOPSIS
    Launches the DJ Scratch Looper WPF application with customizable tracks and images.
     
    .DESCRIPTION
    Creates and displays an interactive DJ scratch looper interface with pitch shifting capabilities.
    Users can specify custom MP3 tracks and button images.
     
    .PARAMETER Tracks
    Array of 9 MP3 file paths for the pads. If not specified, uses defaults.
     
    .PARAMETER BackgroundImage
    Path to background/vinyl image. Defaults to module's included image.
     
    .PARAMETER Button1 through Button9
    Paths to individual pad button images. Defaults to module's included images.
     
    .PARAMETER Title
    Window title. Default: "DJ SCRATCH LOOPER = POWERSHELL MAGIC"
     
    .EXAMPLE
    Invoke-DJScratchLooper -Tracks @("C:\beat1.mp3", "C:\beat2.mp3", ...) -BackgroundImage "C:\custom_bg.png"
    #>

    
    param(
        [Parameter(Mandatory = $false)]
        [string[]]$Tracks,
        
        [Parameter(Mandatory = $false)]
        [string]$BackgroundImage,
        
        [Parameter(Mandatory = $false)]
        [string]$Button1,
        
        [Parameter(Mandatory = $false)]
        [string]$Button2,
        
        [Parameter(Mandatory = $false)]
        [string]$Button3,
        
        [Parameter(Mandatory = $false)]
        [string]$Button4,
        
        [Parameter(Mandatory = $false)]
        [string]$Button5,
        
        [Parameter(Mandatory = $false)]
        [string]$Button6,
        
        [Parameter(Mandatory = $false)]
        [string]$Button7,
        
        [Parameter(Mandatory = $false)]
        [string]$Button8,
        
        [Parameter(Mandatory = $false)]
        [string]$Button9,
        
        [Parameter(Mandatory = $false)]
        [string]$Title = "DJ SCRATCH LOOPER = POWERSHELL MAGIC"
    )
    
    # Get module root directory for default resources
    $moduleRoot = $PSScriptRoot
    
    # Set defaults for images if not provided
    if (-not $BackgroundImage) { $BackgroundImage = Join-Path $moduleRoot "resources\images\bg.png" }
    if (-not $Button1) { $Button1 = Join-Path $moduleRoot "resources\images\pad1.png" }
    if (-not $Button2) { $Button2 = Join-Path $moduleRoot "resources\images\pad2.png" }
    if (-not $Button3) { $Button3 = Join-Path $moduleRoot "resources\images\pad3.png" }
    if (-not $Button4) { $Button4 = Join-Path $moduleRoot "resources\images\pad4.png" }
    if (-not $Button5) { $Button5 = Join-Path $moduleRoot "resources\images\pad5.png" }
    if (-not $Button6) { $Button6 = Join-Path $moduleRoot "resources\images\pad6.png" }
    if (-not $Button7) { $Button7 = Join-Path $moduleRoot "resources\images\pad7.png" }
    if (-not $Button8) { $Button8 = Join-Path $moduleRoot "resources\images\pad8.png" }
    if (-not $Button9) { $Button9 = Join-Path $moduleRoot "resources\images\pad9.png" }
    
    # Set defaults for tracks if not provided
    if (-not $Tracks -or $Tracks.Count -ne 9) {
        $Tracks = @(
            (Join-Path $moduleRoot "resources\beats\beat1.mp3"),
            (Join-Path $moduleRoot "resources\beats\beat2.mp3"),
            (Join-Path $moduleRoot "resources\beats\beat3.mp3"),
            (Join-Path $moduleRoot "resources\beats\beat4.mp3"),
            (Join-Path $moduleRoot "resources\beats\beat5.mp3"),
            (Join-Path $moduleRoot "resources\beats\beat6.mp3"),
            (Join-Path $moduleRoot "resources\beats\beat7.mp3"),
            (Join-Path $moduleRoot "resources\beats\beat8.mp3"),
            (Join-Path $moduleRoot "resources\beats\beat9.mp3")
        )
    }
    
    # Load assemblies
    Add-Type -AssemblyName PresentationFramework
    Add-Type -AssemblyName PresentationCore
    Add-Type -AssemblyName WindowsBase
    
    # Load NAudio if not already loaded
    $naudioPath = Join-Path $moduleRoot "lib\NAudio.dll"
    if (-not ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq "NAudio" })) {
        if (Test-Path $naudioPath) {
            Add-Type -Path $naudioPath
        }
    }
    
    # Load pitch-shift assembly
    $pitchShiftPath = Join-Path $moduleRoot "lib\PitchShiftStep.dll"
    if (Test-Path $pitchShiftPath) {
        Add-Type -Path $pitchShiftPath
    } else {
        Write-Error "PitchShiftStep.dll not found at $pitchShiftPath"
        return
    }
    
    # Build XAML with custom images
    $xaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:media="clr-namespace:System.Windows.Media;assembly=PresentationCore"
        xmlns:effects="clr-namespace:System.Windows.Media.Effects;assembly=PresentationCore"
        xmlns:animation="clr-namespace:System.Windows.Media.Animation;assembly=PresentationCore"
        Title="DJ SCRATCH LOOPER = POWERSHELL MAGIC" Height="690" Width="670">
    <Window.Resources>
        <Style x:Key="PitchButtonNeon" TargetType="Button">
            <Setter Property="Foreground" Value="#00FFFF"/>
            <Setter Property="FontSize" Value="26"/>
            <Setter Property="FontWeight" Value="Bold"/>
            <Setter Property="Padding" Value="20,10"/>
            <Setter Property="Cursor" Value="Hand"/>
            <Setter Property="Background" Value="#111"/>
            <Setter Property="BorderBrush" Value="#00FFFF"/>
            <Setter Property="BorderThickness" Value="3"/>
            <Setter Property="Effect">
                <Setter.Value>
                    <DropShadowEffect Color="#00FFFF" BlurRadius="20" ShadowDepth="0" Opacity="0.8"/>
                </Setter.Value>
            </Setter>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        <Grid>
                            <Border Background="{TemplateBinding Background}"
                                    BorderBrush="{TemplateBinding BorderBrush}"
                                    BorderThickness="{TemplateBinding BorderThickness}"
                                    CornerRadius="12"/>
                            <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
                        </Grid>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter Property="Effect">
                                    <Setter.Value>
                                        <DropShadowEffect Color="#00FFFF" BlurRadius="35" ShadowDepth="0" Opacity="1"/>
                                    </Setter.Value>
                                </Setter>
                            </Trigger>
                            <Trigger Property="IsPressed" Value="True">
                                <Setter Property="RenderTransform">
                                    <Setter.Value>
                                        <ScaleTransform ScaleX="0.95" ScaleY="0.95"/>
                                    </Setter.Value>
                                </Setter>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
 
        <Style x:Key="LedPadStyle" TargetType="Border">
            <Setter Property="CornerRadius" Value="12"/>
            <Setter Property="BorderThickness" Value="4"/>
            <Setter Property="BorderBrush" Value="#444"/>
        </Style>
        <Style x:Key="ArcadeStopButton" TargetType="Button">
            <Setter Property="Foreground" Value="White"/>
            <Setter Property="FontSize" Value="38"/>
            <Setter Property="FontWeight" Value="Bold"/>
            <Setter Property="Padding" Value="20,10"/>
            <Setter Property="Cursor" Value="Hand"/>
            <Setter Property="HorizontalAlignment" Value="Right"/>
            <Setter Property="VerticalAlignment" Value="Center"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        <Grid>
                            <Ellipse x:Name="Glow" Width="140" Height="140" Fill="DarkRed" Stroke="Red" StrokeThickness="4" Opacity="0.7">
                                <Ellipse.Effect>
                                    <DropShadowEffect Color="Red" BlurRadius="25" ShadowDepth="0" Opacity="1"/>
                                </Ellipse.Effect>
                            </Ellipse>
                            <Ellipse x:Name="Bevel" Width="120" Height="120" Fill="DarkRed">
                                <Ellipse.Effect>
                                    <DropShadowEffect Color="#550000" BlurRadius="10" ShadowDepth="4" Opacity="0.8"/>
                                </Ellipse.Effect>
                            </Ellipse>
                            <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
                        </Grid>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter TargetName="Glow" Property="Opacity" Value="1"/>
                                <Setter TargetName="Glow" Property="RenderTransform">
                                    <Setter.Value>
                                        <ScaleTransform ScaleX="1.05" ScaleY="1.05"/>
                                    </Setter.Value>
                                </Setter>
                            </Trigger>
                            <Trigger Property="IsPressed" Value="True">
                                <Setter TargetName="Glow" Property="Opacity" Value="0.3"/>
                                <Setter TargetName="Bevel" Property="RenderTransform">
                                    <Setter.Value>
                                        <ScaleTransform ScaleX="0.95" ScaleY="0.95"/>
                                    </Setter.Value>
                                </Setter>
                                <Setter Property="RenderTransform">
                                    <Setter.Value>
                                        <ScaleTransform ScaleX="0.95" ScaleY="0.95"/>
                                    </Setter.Value>
                                </Setter>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <Grid>
        <!-- VINYL BEHIND EVERYTHING -->
        <Image x:Name="VinylImage"
               Source="$BackgroundImage" Stretch="UniformToFill" RenderTransformOrigin="0.5,0.5" Panel.ZIndex="0">
            <Image.RenderTransform>
                <RotateTransform x:Name="VinylRotate" Angle="0"/>
            </Image.RenderTransform>
        </Image>
        <!-- FOREGROUND UI -->
        <Grid Panel.ZIndex="1">
            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <UniformGrid Rows="3" Columns="3" Margin="10" Grid.Row="0">
                <Button Name="Pad1" Margin="5" Width="110" Height="110">
                    <Border Style="{StaticResource LedPadStyle}">
                        <Image Source="$Button1" Width="110" Height="110" Stretch="Uniform"/>
                    </Border>
                </Button>
                <Button Name="Pad2" Margin="5" Width="110" Height="110">
                    <Border Style="{StaticResource LedPadStyle}">
                        <Image Source="$Button2" Width="110" Height="110" Stretch="Uniform"/>
                    </Border>
                </Button>
                <Button Name="Pad3" Margin="5" Width="110" Height="110">
                    <Border Style="{StaticResource LedPadStyle}">
                        <Image Source="$Button3" Width="110" Height="110" Stretch="Uniform"/>
                    </Border>
                </Button>
                <Button Name="Pad4" Margin="5" Width="110" Height="110">
                    <Border Style="{StaticResource LedPadStyle}">
                        <Image Source="$Button4" Width="110" Height="110" Stretch="Uniform"/>
                    </Border>
                </Button>
                <Button Name="Pad5" Margin="5" Width="110" Height="110">
                    <Border Style="{StaticResource LedPadStyle}">
                        <Image Source="$Button5" Width="110" Height="110" Stretch="Uniform"/>
                    </Border>
                </Button>
                <Button Name="Pad6" Margin="5" Width="110" Height="110">
                    <Border Style="{StaticResource LedPadStyle}">
                        <Image Source="$Button6" Width="110" Height="110" Stretch="Uniform"/>
                    </Border>
                </Button>
                <Button Name="Pad7" Margin="5" Width="110" Height="110">
                    <Border Style="{StaticResource LedPadStyle}">
                        <Image Source="$Button7" Width="110" Height="110" Stretch="Uniform"/>
                    </Border>
                </Button>
                <Button Name="Pad8" Margin="5" Width="110" Height="110">
                    <Border Style="{StaticResource LedPadStyle}">
                        <Image Source="$Button8" Width="110" Height="110" Stretch="Uniform"/>
                    </Border>
                </Button>
                <Button Name="Pad9" Margin="5" Width="110" Height="110">
                    <Border Style="{StaticResource LedPadStyle}">
                        <Image Source="$Button9" Width="110" Height="110" Stretch="Uniform"/>
                    </Border>
                </Button>
            </UniformGrid>
            <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right" Margin="20">
                <Button Name="PitchDown" Content="Pitch -" Width="140" Height="60" Margin="10" Style="{StaticResource PitchButtonNeon}"/>
                <Button Name="PitchUp" Content="Pitch +" Width="140" Height="60" Margin="10" Style="{StaticResource PitchButtonNeon}"/>
                <Button Name="StopButton" Content="STOP" Style="{StaticResource ArcadeStopButton}" Margin="20"/>
            </StackPanel>
        </Grid>
    </Grid>
</Window>
"@

#endregion

#region Window creation and application setup

# Load XAML
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]$xaml)
$window = [Windows.Markup.XamlReader]::Load($reader)

# Stop audio when window closes
$window.Add_Closing({
    Stop-AllLoops
})

$global:vinylanime = 6
#endregion

#region Animation helpers
# This function animates the vinyl spinning
function Start-VinylSpin {
    $rotate = $window.FindName("VinylRotate")
    $animation = New-Object System.Windows.Media.Animation.DoubleAnimation
    $animation.From = 0
    $animation.To = 360
    $animation.Duration = [System.Windows.Duration]::new([System.TimeSpan]::FromSeconds($($global:vinylanime)))
    $animation.RepeatBehavior = [System.Windows.Media.Animation.RepeatBehavior]::Forever

    $rotate.BeginAnimation([System.Windows.Media.RotateTransform]::AngleProperty, $animation)
}

# This function stops the vinyl animation
function Stop-VinylSpin {
    $rotate = $window.FindName("VinylRotate")
    # Passing $null stops the animation cleanly
    $rotate.BeginAnimation([System.Windows.Media.RotateTransform]::AngleProperty, $null)
}
#endregion

#region Visual helpers

# This function will set the glow effect on a pad button when it's active
function Set-PadGlow {
    param($padName, $active)

    $border = $window.FindName($padName).Content

    if ($active) {
        $effect = New-Object System.Windows.Media.Effects.DropShadowEffect
        $effect.Color = "#FFFF33"
        $effect.BlurRadius = 25
        $effect.ShadowDepth = 0
        $effect.Opacity = 1
        $border.Effect = $effect
    }
    else {
        $border.Effect = $null
    }
}
#endregion

#region Audio playback helpers
# Store active players
# Dictionary of active audio players keyed by file path.
$global:Players = @{}
# This function will start playing an MP3 file in a loop, stopping any currently playing loops
function Start-Mp3Loop {
    param($Path)

    # Stop ALL currently playing loops
    foreach ($key in $Players.Keys) {
        try {
            $Players[$key].Stop()
            $Players[$key].Dispose()
        } catch {}
    }
    $Players.Clear()

    # Create new pitch-enabled player
    $player = New-Object PitchShiftStepLib.StepPitchPlayer($Path)

    # Play
    $player.Play()
    $global:vinylanime = 6
    # Start vinyl animation
    Start-VinylSpin

    # Store player
    $global:Players[$Path] = $player
}


# This function will stop all currently playing loops and clear the Players dictionary
function Stop-AllLoops {
    foreach ($player in $Players.Values) {
        try { $player.Stop() } catch {}
        try { $player.Dispose() } catch {}
    }

    $Players.Clear()
    $global:vinylanime = 6
    Stop-VinylSpin
}
#endregion

#region Beat mapping
# Beat map, mapping pad names to MP3 file paths
if ($Tracks -and $Tracks.Count -eq 9) {
    $beats = @{
        Pad1 = $Tracks[0]
        Pad2 = $Tracks[1]
        Pad3 = $Tracks[2]
        Pad4 = $Tracks[3]
        Pad5 = $Tracks[4]
        Pad6 = $Tracks[5]
        Pad7 = $Tracks[6]
        Pad8 = $Tracks[7]
        Pad9 = $Tracks[8]
    }
} else {
    $beats = @{
        Pad1 = Join-Path $moduleRoot "resources\beats\beat1.mp3"
        Pad2 = Join-Path $moduleRoot "resources\beats\beat2.mp3"
        Pad3 = Join-Path $moduleRoot "resources\beats\beat3.mp3"
        Pad4 = Join-Path $moduleRoot "resources\beats\beat4.mp3"
        Pad5 = Join-Path $moduleRoot "resources\beats\beat5.mp3"
        Pad6 = Join-Path $moduleRoot "resources\beats\beat6.mp3"
        Pad7 = Join-Path $moduleRoot "resources\beats\beat7.mp3"
        Pad8 = Join-Path $moduleRoot "resources\beats\beat8.mp3"
        Pad9 = Join-Path $moduleRoot "resources\beats\beat9.mp3"
    }
}
#endregion

#region Button bindings
# Bind pad buttons (use sender.Name to avoid closure issues)
foreach ($pad in $beats.Keys) {
    $button = $window.FindName($pad)
    if ($button) {
        $button.Add_Click({
            param($sender, $args)

            foreach ($padName in $beats.Keys) {
                Set-PadGlow $padName $false
            }

            Set-PadGlow $sender.Name $true
            Start-Mp3Loop $beats[$sender.Name]
        })
    }
}

# Bind stop and pitch control buttons from the UI.
$stopButton = $window.FindName("StopButton")
$pitchUp = $window.FindName("PitchUp")
$pitchDown = $window.FindName("PitchDown")

$pitchUp.Add_Click({
    $Players.Values | ForEach-Object { $_.IncreasePitchStep() }
    if ($($global:vinylanime) -gt 1)
    {
        $global:vinylanime--
        Start-VinylSpin
    }
    else{$global:vinylanime = 1;Start-VinylSpin}
   
})

$pitchDown.Add_Click({
    $Players.Values | ForEach-Object { $_.DecreasePitchStep() }
    if ($($global:vinylanime) -ge 1)
    {
        $global:vinylanime++
        Start-VinylSpin
    }
    elseif($($global:vinylanime) -le 10){$global:vinylanime = 10;Start-VinylSpin}
    else{$global:vinylanime++;Start-VinylSpin}

})



# Stop the music when the STOP button is clicked
$stopButton.Add_Click({
    Stop-AllLoops
})
#endregion

#region Application launch
# Show the window and start the WPF message loop.
$window.ShowDialog()
#endregion
}