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 } # SIG # Begin signature block # MIIFlAYJKoZIhvcNAQcCoIIFhTCCBYECAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUHeWBz1FF9sRXFtvXLxrxWD85 # kLCgggMiMIIDHjCCAgagAwIBAgIQKygqk+Hu0LFGE87oIwmfeTANBgkqhkiG9w0B # AQsFADAnMSUwIwYDVQQDDBxQb3dlclNoZWxsIENvZGUgU2lnbmluZyBDZXJ0MB4X # DTI2MDgxMTA4MzU1MloXDTI3MDgxMTA4NTU1MlowJzElMCMGA1UEAwwcUG93ZXJT # aGVsbCBDb2RlIFNpZ25pbmcgQ2VydDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC # AQoCggEBALfiE4TY1ZlzG4cDqC+DBjAtyZVajc3ymvhh+BuQQeskJtl3ebH488bz # c3Vqn5YOHptWAsGrGLjzG0AoMNjD5bkpm6PXyu95mID+P4C/df5s4pMnSombz6WF # dp1Q/h5uZ1P/DT43V9/wM6gveploDzV64sETRngK/bK3Gt34irrsZ3rGVIhXuYQW # LFyVW2Lr9oC5gxP38uYlOreVpGjfU6zhTa5i9bFznT1rp4uNNvsfZQ4XL/i6YuYr # e5xL3Ce/0Y2kZ1R6E/frflL/Zbx6NJPNSgQP63ygARJkyLIxxrnNPu6gSUf891q5 # M4NYYUv3KXmjtny7eknHmOv22Vr58WUCAwEAAaNGMEQwDgYDVR0PAQH/BAQDAgeA # MBMGA1UdJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBTzFZvH6j85EwCUy9mJlhuP # SNSD9TANBgkqhkiG9w0BAQsFAAOCAQEAf6tuM+A3fIpjc682oAM65d25cBSPIukv # IxUGQCk0MAn6aj7GizCoaljU9V0jls8iMiEoq/HilOsEuup7j6MiREONZ82Vr7t8 # NxkZqFRDEp9VR5bW08m1ThcrAS/U9m5tl/5bcTTpBaxZE9472FB5gIeQFAqglykj # CCMaHuC0xLB/2HOvVsTW/ISZOkDD0n7KpBQ0A6A7P6rnJylPBJuMk3eX9UqbNUx/ # VgDJ7vWquLbJy6U7I3Hdk84wT0NXJ0bi0FWYEgNMVK3L0nND0BRXhulgRPVx0c/N # vxRs+rxBB912ifJjQbdyowdnMyFYbzIvtx5GkB0NmprIrlNJksVeQjGCAdwwggHY # AgEBMDswJzElMCMGA1UEAwwcUG93ZXJTaGVsbCBDb2RlIFNpZ25pbmcgQ2VydAIQ # Kygqk+Hu0LFGE87oIwmfeTAJBgUrDgMCGgUAoHgwGAYKKwYBBAGCNwIBDDEKMAig # AoAAoQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgEL # MQ4wDAYKKwYBBAGCNwIBFTAjBgkqhkiG9w0BCQQxFgQURK5nTWctr12UN8H37y13 # bso1vdcwDQYJKoZIhvcNAQEBBQAEggEAElsVTP1oRF8w+l2Bgxljrqa9hpdtDuQF # I2K1TxIIgTtwbZQDUkQJg1odMdr9FYvIEXBY2A98NjR9aGH0S/3QLsFWt5ut+RjR # sqYxNnd5xQBPczziyx+jSoARbwZxvO0bbn0GDZUHzc6wm9VF7zasql5Z7KwQ55yU # UrVMKHTnCVaDr+YNhElkGYMtLvz31+mjcsyEE+s1KPYhHltkiV7deKKZcxddyTST # W08kQCDRn2LimrL4y664TKw9Mhf1mqUbF8jnhbwvtpqATZkNWCYiUISDREDg2jyZ # HR0x4kDNStalgk9A/hPAumY/Da4r1HipxCin0drD8VUtqnnY6ikj2A== # SIG # End signature block |