Types/Wave/DECPS.ps1
|
<# .SYNOPSIS DECPS Wave Generator .DESCRIPTION DECPS is an ancient console audio format that encodes audio in escape sequences. Each contains: * An Escape Character (`[char]27`) * An open bracket * A volume (0-7) * A fraction (32nds of a second) * A series of notes in a two octave range (between C5 and C7), separated by `;` * A closing ~ For example: ~~~PowerShell "`e[3;2;1;2;3~" # Volume of 3, Fraction of 2, notes 1, 2, 3 (C5, C#5, D5) ~~~ This converts DECPS notes into a wave form. .NOTES DECPS is a format originally supported by the [DEC VT250](https://en.wikipedia.org/wiki/VT520). It is also supported in some moderns Terminal (Windows Terminal, Contour) Because DECPS dates from a time where computers could only play higher frequencies, this also shifts the octave when converting notes. By default, this shifts notes down by two octaves, into a more "normal" C3-C5 range. .LINK https://web.mit.edu/dosathena/doc/www/ek-vt520-rm.pdf .LINK https://tightloop.io/terminal-decps/ .EXAMPLE $tune = "`e[3;2;1;3;5,~`e[3;8;6;10;8;11,~`e[3;4;10;11;13;10,~`e[3;8;8;11,~`e[3;4;10;11;13;10;11;13;15;17,~`e[3;8;18;17;18,~" $wav = wav DECPS $tune $wav.Play() .EXAMPLE $shortMarch = @( "`e[3;20;7;7;7,~" "`e[3;15;3,~" "`e[3;5;10,~" "`e[3;20;7,~" "`e[3;15;3,~" "`e[3;5;10,~" "`e[3;40;7,~" ) -join [Environment]::Newline wave DECPS $shortMarch play .EXAMPLE $closeWave = @( "`e[3;10;15;17;13;2;8,~" ) wave DECPS "`e[3;12;15;17;13;2;8,~" play .EXAMPLE $escape = [char]27 $march = @( "$escape[3;20;7;7;7,~", "$escape[3;15;3,~", "$escape[3;5;10,~", "$escape[3;20;7,~", "$escape[3;15;3,~", "$escape[3;5;10,~", "$escape[3;40;7,~", "$escape[3;20;14;14;14,~", "$escape[3;15;15,~", "$escape[3;5;10,~", "$escape[3;20;6,~", "$escape[3;15;3,~", "$escape[3;5;10,~", "$escape[3;40;7,~", "$escape[3;20;19;7;19,~", "$escape[3;10;18;17,~", "$escape[3;5;16;15,~", "$escape[3;10;16;0;8,~", "$escape[3;20;13,~", "$escape[3;10;12;11,~", "$escape[3;5;10;9,~", "$escape[3;10;10;0;3,~", "$escape[3;20;6,~", "$escape[3;15;3,~", "$escape[3;5;10,~", "$escape[3;20;7,~", "$escape[3;15;3,~", "$escape[3;5;10,~", "$escape[3;40;7,~" ) -join [Environment]::Newline $wav = wave $march $wav.play() .EXAMPLE #> param( # The DECPS note sequence [string] $Sequence, # The number of octaves to shift. By default -2. [int] $ShiftOctave = -1, # The amount of time to generate per note. [Timespan]$Duration = $( if ($this.BPM -is [TimeSpan]) {$this.BPM} else { [TimeSpan]::FromSeconds(1) } ) ) <# We will approach this in three phases: * Phase 1: Convert DECPS escape sequences to notes * Phase 2: Convert Notes to Waveforms * Phase 3: Profit #> # Matching DECPS is easy: # look for escape + open bracket, yada yada yada, `,~` $decps = @([Regex]::Matches($Sequence, '\e\[.+?,~')) # DECPS notes are integers. # It's fast if we just find use the index as an offset. $notelist = @( "~" "c5", "c♯5", "d5", "d♯5", "e5", "f5", "f♯5", "g5", "g♯5", "a5", "a♯5", "b5" "c6", "c♯6", "d6", "d♯6", "e6", "f6", "f♯6", "g6", "g♯6", "a6", "a♯6", "b6" "c7" ) # Get our note frequency table $noteFrequency = $this.NoteFrequency # Prepare a progress bar $progress = @{ id = Get-Random status = 'Converting' activity = ' ' } # And prepare to generate events. # (We may as well log what happens, as it gives us history and readable note sequences) $events = [Runspace]::DefaultRunspace.Events $NoteSequence = @( # Walk over our array of DECPS notes for ($index = 0; $index -lt $decps.Length; $index++) { $match = $decps[$index] $notes = @() # writing progress as we go # (though it will probably be too quick to see) $progress.PercentComplete = $index * 100 / $decps.Length $progress.Activity = "$index / $($decps.Length)" Write-Progress @progress # A bit of multiple assignment lets us pick out the notes. $volume, $time, $notes = $match -replace '^\e\[' -replace ',~$' -split ';' # If we have no notes, continue if (-not $notes) { continue } # Volume is a value between 0-7. # Treat it as the fraction it is. $volume = ($volume -as [byte])/7 # Each note is represented as a 32nd of a second $fraction = $time / 32 # If we provided a duration, # this will be used as the beat base instead. # (Duration defaults to 1 second, the DECPS timebase) $noteDuration = $Duration * $fraction # Turn our notes into friendly notes $friendlyNote = foreach ($note in $notes) { # First make each note an index $note = $note -as [int] # Then, if we have that index in our note list if ($note -and $notelist[$note]) { $notelist[$note] # that is our friendly note. } } # Walk over each friendly note foreach ($friendly in $friendlyNote) { $friendly = $friendly -replace '#', '♯' -creplace 'b', '♭' # and make it into a dictionary [Ordered]@{ Name = $friendly; Frequency = $noteFrequency[$friendly] Duration = $noteDuration; Volume = $volume; ShiftOctave = $ShiftOctave } } } ) # Generate a `Tune` event. # This contains the note sequence # before any instruments are applied and any tones are generated $null = $events.GenerateEvent( 'Tune', $this, $NoteSequence, ( [Ordered]@{} + $PSBoundParameters ) ) if ($this) { $this.Melody += @( foreach ($note in $NoteSequence) { $note.PSTypeName = 'Note' [PSCustomObject]$note } ) } # Last but not least, complete our progress bars, $progress.Remove('PercentComplete') $progress.Completed = $true Write-Progress @progress # and return our sound with any instruments. return $this.Sound() |