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" .EXAMPLE Invoke-DJScratchLooper #> 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" Title="DJ SCRATCH LOOPER = POWERSHELL MAGIC" Height="690" Width="685" WindowStyle="None" AllowsTransparency="True" Background="Transparent"> <Window.Resources> <!-- Window Glow Border --> <Style x:Key="WindowGlowBorder" TargetType="Border"> <Setter Property="CornerRadius" Value="40"/> <Setter Property="BorderThickness" Value="5"/> <Setter Property="BorderBrush" Value="#00FFFF"/> <Setter Property="Background" Value="#FFFFFF"/> <Setter Property="Cursor" Value="Hand"/> <Setter Property="Effect"> <Setter.Value> <DropShadowEffect Color="#00FFFF" BlurRadius="20" ShadowDepth="0" Opacity="0.8"/> </Setter.Value> </Setter> </Style> <!-- Pitch Buttons --> <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="18"/> <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> <!-- LED Pad Border --> <Style x:Key="LedPadStyle" TargetType="Border"> <Setter Property="CornerRadius" Value="12"/> <Setter Property="BorderThickness" Value="4"/> <Setter Property="BorderBrush" Value="#444"/> </Style> <!-- Arcade Stop Button --> <Style x:Key="ArcadeStopButton" TargetType="Button"> <Setter Property="Foreground" Value="White"/> <Setter Property="FontSize" Value="24"/> <Setter Property="FontWeight" Value="Bold"/> <Setter Property="Cursor" Value="Hand"/> <Setter Property="Background" Value="Red"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="Button"> <Grid Width="120" Height="120"> <!-- Outer Glow --> <Ellipse x:Name="Glow" Width="120" Height="120" Opacity="0.7"> <Ellipse.Fill> <RadialGradientBrush> <GradientStop Color="#FFFF6666" Offset="0"/> <GradientStop Color="#00FF0000" Offset="1"/> </RadialGradientBrush> </Ellipse.Fill> <Ellipse.Effect> <DropShadowEffect Color="#FF0000" BlurRadius="30" ShadowDepth="0" Opacity="1"/> </Ellipse.Effect> </Ellipse> <!-- Metal Bezel --> <Ellipse Width="100" Height="100" StrokeThickness="4"> <Ellipse.Fill> <RadialGradientBrush> <GradientStop Color="#D0D0D0" Offset="0"/> <GradientStop Color="#707070" Offset="1"/> </RadialGradientBrush> </Ellipse.Fill> <Ellipse.Stroke> <SolidColorBrush Color="#555555"/> </Ellipse.Stroke> </Ellipse> <!-- Main Button --> <Ellipse x:Name="ButtonFace" Width="80" Height="80" Stroke="#770000" StrokeThickness="3"> <Ellipse.Fill> <RadialGradientBrush Center="0.15,0.15" RadiusX="0.6" RadiusY="0.6"> <GradientStop Color="#FF6666" Offset="0"/> <GradientStop Color="#DD0000" Offset="0.2"/> <GradientStop Color="#770000" Offset="1"/> </RadialGradientBrush> </Ellipse.Fill> <Ellipse.Effect> <DropShadowEffect Color="#550000" BlurRadius="6" ShadowDepth="1" Opacity="0.8"/> </Ellipse.Effect> </Ellipse> <!-- Highlight --> <Ellipse Width="15" Height="5" Margin="-5,-17,0,0" VerticalAlignment="Center" HorizontalAlignment="Center" Opacity="0.35" Fill="White"/> <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/> </Grid> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter TargetName="Glow" Property="Opacity" Value="1"/> <Setter TargetName="ButtonFace" Property="RenderTransform"> <Setter.Value> <ScaleTransform ScaleX="1.03" ScaleY="1.03"/> </Setter.Value> </Setter> </Trigger> <Trigger Property="IsPressed" Value="True"> <Setter TargetName="Glow" Property="Opacity" Value="0.3"/> <Setter TargetName="ButtonFace" Property="RenderTransform"> <Setter.Value> <ScaleTransform ScaleX="0.98" ScaleY="0.98"/> </Setter.Value> </Setter> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> </Window.Resources> <Border Opacity="0.85" Style="{StaticResource WindowGlowBorder}"> <Grid> <!-- Vinyl Background --> <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> <!-- Close Button --> <Button Name="CloseButton" Content="X" Width="26" Height="26" FontSize="18" FontWeight="Bold" Foreground="White" Background="Red" BorderBrush="Red" Cursor="Hand" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="15"> <Button.Template> <ControlTemplate TargetType="Button"> <Border x:Name="Border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="1" CornerRadius="8"> <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/> </Border> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter TargetName="Border" Property="Background" Value="Blue"/> <Setter Property="Foreground" Value="White"/> <Setter TargetName="Border" Property="BorderBrush" Value="Red"/> </Trigger> <Trigger Property="IsPressed" Value="True"> <Setter TargetName="Border" Property="Background" Value="#FFDDDD"/> <Setter Property="Foreground" Value="Red"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Button.Template> </Button> <!-- 3�3 Pad Grid --> <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" Stretch="Uniform"/> </Border> </Button> <Button Name="Pad2" Margin="5" Width="110" Height="110"> <Border Style="{StaticResource LedPadStyle}"> <Image Source="$Button2" Stretch="Uniform"/> </Border> </Button> <Button Name="Pad3" Margin="5" Width="110" Height="110"> <Border Style="{StaticResource LedPadStyle}"> <Image Source="$Button3" Stretch="Uniform"/> </Border> </Button> <Button Name="Pad4" Margin="5" Width="110" Height="110"> <Border Style="{StaticResource LedPadStyle}"> <Image Source="$Button4" Stretch="Uniform"/> </Border> </Button> <Button Name="Pad5" Margin="5" Width="110" Height="110"> <Border Style="{StaticResource LedPadStyle}"> <Image Source="$Button5" Stretch="Uniform"/> </Border> </Button> <Button Name="Pad6" Margin="5" Width="110" Height="110"> <Border Style="{StaticResource LedPadStyle}"> <Image Source="$Button6" Stretch="Uniform"/> </Border> </Button> <Button Name="Pad7" Margin="5" Width="110" Height="110"> <Border Style="{StaticResource LedPadStyle}"> <Image Source="$Button7" Stretch="Uniform"/> </Border> </Button> <Button Name="Pad8" Margin="5" Width="110" Height="110"> <Border Style="{StaticResource LedPadStyle}"> <Image Source="$Button8" Stretch="Uniform"/> </Border> </Button> <Button Name="Pad9" Margin="5" Width="110" Height="110"> <Border Style="{StaticResource LedPadStyle}"> <Image Source="$Button9" Stretch="Uniform"/> </Border> </Button> </UniformGrid> <!-- Pitch + Stop Row --> <Grid Grid.Row="1" Margin="10"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="80"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <!-- Volume Control --> <StackPanel Grid.Row="1" Grid.Column="0" HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="5"> <TextBlock Text="VOL" FontSize="12" FontWeight="Bold" Foreground="#00FFFF" HorizontalAlignment="Center"/> <Slider Name="VolumeSlider" Orientation="Vertical" Minimum="0" Maximum="1" Value="0.75" Height="120" Width="30" TickFrequency="0.1" IsSnapToTickEnabled="False"/> </StackPanel> <TextBlock Name="TrackStatus" Grid.Row="0" Grid.ColumnSpan="2" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="150,0,0,10" FontSize="22" FontWeight="Bold" FontFamily="Consolas" Foreground="#00FF00" Text="No Track Loaded"> <TextBlock.Effect> <DropShadowEffect Color="#00FF00" BlurRadius="15" ShadowDepth="0" Opacity="1"/> </TextBlock.Effect> </TextBlock> <StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Center" Margin="72,0,0,0"> <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}"/> </StackPanel> <Button Name="StopButton" Grid.Row="1" Grid.Column="2" Content="STOP" Style="{StaticResource ArcadeStopButton}" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="30,0,0,0"/> </Grid> </Grid> </Grid> </Border> </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) # Make window draggable $window.Add_MouseLeftButtonDown({ $window.DragMove() }) # Close the window as using X $window.FindName("CloseButton").Add_Click({ Stop-AllLoops $window.Close() }) # Stop audio when window closes double check if used alt+f4 to close $window.Add_Closing({ Stop-AllLoops }) # Setting globalk variables to calculate vinyl speed $global:vinylanime = 5 $global:MinPitchSpeed = 1 # 4 clicks up from 5 -> 1 $global:MaxPitchSpeed = 9 # 4 clicks down from 5 -> 9 $global:PitchLevel = 0 $global:CurrentTrack = "None" # Set volume $global:VolumeLevel = 0.75 #endregion #region Animation helpers #This function hides the pitch buttons if maximum speed has been reached function Update-PitchButtons { if ($global:PitchLevel -ge 4) { $pitchUp.Visibility = [System.Windows.Visibility]::Collapsed } else { $pitchUp.Visibility = [System.Windows.Visibility]::Visible } if ($global:PitchLevel -le -4) { $pitchDown.Visibility = [System.Windows.Visibility]::Collapsed } else { $pitchDown.Visibility = [System.Windows.Visibility]::Visible } } # 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 = @{} $script:OutputField = [PitchShiftStepLib.StepPitchPlayer].GetField("output",[System.Reflection.BindingFlags]"Instance,NonPublic") # This function controls volume function Set-PlayerVolume { param( $Player, [float]$Volume ) try { $waveOut = $script:OutputField.GetValue($Player) if ($waveOut) { $waveOut.Volume = $Volume } } catch {} } # This function will start playing an MP3 file in a loop, stopping any currently playing loops function Start-Mp3Loop { param($Path) foreach ($key in $Players.Keys) { try { $Players[$key].Stop() $Players[$key].Dispose() } catch {} } $Players.Clear() $player = New-Object PitchShiftStepLib.StepPitchPlayer($Path) Set-PlayerVolume -Player $player -Volume $global:VolumeLevel $player.Play() $global:PitchLevel = 0 $global:vinylanime = 5 Update-PitchButtons Start-VinylSpin $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:CurrentTrack = "None" $global:PitchLevel = 0 $global:vinylanime = 5 Update-PitchButtons 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" } } # Music track mapping $trackNames = @{ Pad1 = "Track One" Pad2 = "Track Two" Pad3 = "Track Three" Pad4 = "Track Four" Pad5 = "Track Five" Pad6 = "Track Six" Pad7 = "Track Seven" Pad8 = "Track Eight" Pad9 = "Track Nine" } #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 $global:CurrentTrack = $trackNames[$sender.Name] $trackStatus.Text = "Playing $($global:CurrentTrack)" 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") $trackStatus = $window.FindName("TrackStatus") $volumeSlider = $window.FindName("VolumeSlider") $volumeSlider.Value = $global:VolumeLevel # Update the pitch buttons Update-PitchButtons $volumeSlider.Add_ValueChanged({ $global:VolumeLevel = [float]$volumeSlider.Value $Players.Values | ForEach-Object { Set-PlayerVolume -Player $_ -Volume $global:VolumeLevel } }) # Add click functionality pitch down $pitchUp.Add_Click({ if ($global:PitchLevel -lt 4) { $Players.Values | ForEach-Object { $_.IncreasePitchStep() } $global:PitchLevel++ if ($global:vinylanime -gt 1) { $global:vinylanime-- Start-VinylSpin } Update-PitchButtons } }) # Add click functionality pitch down $pitchDown.Add_Click({ if ($global:PitchLevel -gt -4) { $Players.Values | ForEach-Object { $_.DecreasePitchStep() } $global:PitchLevel-- if ($global:vinylanime -lt 10) { $global:vinylanime++ Start-VinylSpin } Update-PitchButtons } }) # Stop the music when the STOP button is clicked $stopButton.Add_Click({ if ($global:CurrentTrack -ne "None") { $trackStatus.Text = "Stopped $($global:CurrentTrack)" } else { $trackStatus.Text = "No Track Loaded" } Stop-AllLoops }) #endregion #region Application launch # Show the window and start the WPF message loop. $window.ShowDialog() #endregion } |