shithub: pt2-clone

Download patch

ref: 71666cfc9fdb10b223032a30a4c0862af49c1e33
parent: b2d2be285518b1ea8d0db23cc653861ce4222f44
author: Olav Sørensen <[email protected]>
date: Thu May 28 19:57:52 EDT 2020

Pushed v1.17 code

- Added audio sampling capability. It can be accessed by clicking the new
  "SAMPLE" button in the SAMPLER screen. Pressing the right mouse button is
  the same as pressing the [SAMPLE] button, which is handy for easier timing.
  This is experimental and can contain bugs! Let me know if you find any.
- Fixed a bug where clicking to the left of the song/sample name could cause
  a temporary UI lock up.
- Bugfix: After loading a new sample, the sample restore (CTRL+Z) buffer
  would not be updated.
- The scopes, VU-meters and (fake) spectrum analyzer should now be less delayed,
  and better in sync with the music. Only a slight difference though.
- The PAT2SMP HI mode was changed from A-3 finetune +5 (28836.54Hz, period 123)
  to A-3 finetune +4 (28603.99Hz, period 124). After reading the Amiga Hardware
  Reference Manual and doing some basic testing, I found out that this rate is
  not safe for channel #4 on a real Amiga. The absolute minium safe period is
  period 124 (28603.991Hz, PAL).
- Very minor code cleanup/changes

--- a/release/help.txt
+++ b/release/help.txt
@@ -1,22 +1,21 @@
  == Frequently Asked Questions (FAQ) ==
  
- * How do I change the Multi channel ordering?
- - When in idle mode (not edit/play etc), press ALT+1/2/3/4 to increase
-   the according multi slot. CTRL+M enables multi.
-   Multi
-   
+  * Is there a way to make the window bigger?
+ - Set VIDEOSCALE to 2X, 3x or 4X in protracker.ini.
+ 
+ * Is there a way to run the program in fullscreen mode?
+ - Press F11. On some keyboards/configurations you may need to press fn+F11.
+ 
  * Alt+F5/ALT+F5 (copy/paste) doesn't work!
  - Windows: you need to make sure those keybinding are disabled in
    'GeForce Experience' if you have that software installed.
  - OS X/mac OS/Linux: You need to remove or re-bind ALT+F4/ALT+F5 to other keys
    in your OS settings.
- 
- * Is there a way to make the window bigger?
- - Set VIDEOSCALE to 2X, 3x or 4X in protracker.ini.
-
- * Is there a way to run the program in fullscreen mode?
- - Press F11.
-
+   
+ * Is there a quicker way to save my module?
+ - Press CTRL+S. This hotkey is "split mode" in Amiga ProTracker, which is not
+   included in this PT clone.
+   
  * How do I change the stereo separation (panning)?
  - Adjust the STEREOSEPARATION setting in protracker.ini. It's a percentage
    value ranging from 0 to 100. 0 being mono and 100 being 100% separated like
@@ -23,6 +22,16 @@
    the audio output from an Amiga.
    Pressing SHIFT+F12 will toggle between STEREOSEPARATION and 100% (hard pan).
 
+ * How do I change the Multi channel ordering?
+ - When in idle mode (not edit/play etc), press ALT+1/2/3/4 to increase
+   the according multi slot. CTRL+M enables multi.
+ 
+ * Is there a more practical way to sample audio than clicking on "Sample"?
+ - Press the right mouse button. This means that you don't have to look at the
+   tracker to see where you are pressing, while you are getting ready to start
+   triggering the sampling source. Pressing the right mouse button again will
+   stop it before the buffer got filled.
+
  * Can this ProTracker clone load PT.Config files?
  - Yes. Put one in the same directory as the executable program, and it will
    load the first one it can find (PT.Config-00..99).
@@ -30,13 +39,10 @@
 
  * Will there ever be MIDI support?
  - No, sorry. Try a modern module tracker instead, or ProTracker on the Amiga.
- 
- * Is there a quick way to save my module?
- - Press CTRL+S. This hotkey is "split mode" in Amiga ProTracker, which is not
-   included in this PT clone.
    
  * Can I revert a sample after I edited it?
- - Press CTRL+Z while the sampler screen is open.
+ - Press CTRL+Z while the sampler screen is open. Keep in mind that this
+   function is extremely limited and will only revert to the last loaded sample.
 
  * [insert random question]
  - Try to send an email to [email protected] or visit #protracker at IRCnet.
@@ -43,13 +49,14 @@
 
  == Some misc. info ==
 
- WARNING: Note B-3 (or pitches getting near; pitch slides, vibrato or arpeggio)
- may not play correctly on a real Amiga. It might crackle/pop because of a
- DMA underrun. Take extra care when doing high pitches!
+ WARNING: Any pitch *higher* than A-3 finetune +4 (period 124, 28603.99Hz)
+ may not play correctly on a real Amiga. It might crackle/pop because of a DMA
+ underrun. This also applies to any pattern effect that changes the pitch.
+ Take extra care when doing high pitches!
 
  == Missing things that will NOT be added in the future ==
   
-  * Split keys (kinda pointless without MIDI). ctrl+s is now used for MOD saving
+  * Split keys (kinda pointless without MIDI). CTRL+S is now used for MOD saving
   * MIDI support
   * Setup screen
   
@@ -157,10 +164,10 @@
  This tool is handy for making drum loops.
  
  - Quality mode: -
- HI: 28836Hz - Play the new sample at note A-3 (finetune +5)
+ HI: 28604Hz - Play the new sample at note A-3 (finetune +4)
  LO: 22168Hz - Play the new sample at note F-3 (finetune +1)
   
- HI has the highest possible quality, but can only fit 2.27 seconds of audio.
+ HI has the highest possible quality, but can only fit 2.29 seconds of audio.
  LO is worse in quality, but can fit slightly more at 2.95 seconds.
  
  You should use this feature in sequences to fit as much rows as you need.
--- a/release/macos/protracker.ini
+++ b/release/macos/protracker.ini
@@ -79,8 +79,6 @@
 ;
 HWMOUSE=TRUE
 
-
-
 [GENERAL SETTINGS]
 ; Hide last modification dates in Disk Op. to get longer dir/file names
 ;        Syntax: TRUE or FALSE
@@ -198,12 +196,10 @@
 ;
 MODDOT=FALSE
 
-; Dotted line in center of sample data view
+; Draw a center line in the SAMPLER screen's waveform
 ;        Syntax: TRUE or FALSE
 ; Default value: TRUE
-;       Comment: Setting it to FALSE will turn off the dotted center line
-;         that is rendered in the middle of the sampler data view in
-;         the sampler screen.
+;       Comment: This used to draw a dotted line, but now draws a line instead
 ;
 DOTTEDCENTER=TRUE
 
@@ -211,20 +207,37 @@
 ; Audio output frequency
 ;        Syntax: Number, in hertz
 ; Default value: 48000
-;       Comment: Ranges from 44100 to 192000.
-;         Also sets the playback frequency for WAVs made with MOD2WAV.
+;       Comment: Ranges from 44100 to 192000. 96000 is recommended if your
+;         OS is set to mix shared audio at 96kHz or higher.
 ;
 FREQUENCY=48000
 
+; Audio input frequency
+;        Syntax: Number, in hertz
+; Default value: 44100
+;       Comment: Ranges from 44100 to 192000. This should be set to match
+;         the frequency used for your audio input device (for sampling).
+;
+SAMPLINGFREQ=44100
+
+; Normalize sampled audio before converting to 8-bit
+;        Syntax: TRUE or FALSE
+; Default value: TRUE
+;       Comment: This one is for the audio sampling feature in the SAMPLER
+;         screen. If it's set to TRUE, it will normalize the gain before it
+;         converts the sample to 8-bit in the end. This will preserve as much
+;         amplitude information as possible to lower quantization noise.
+;
+NORMALIZESAMPLING=TRUE
+
 ; Audio buffer size
 ;        Syntax: Number, in samples
 ; Default value: 1024
 ;       Comment: Ranges from 128 to 8192. Should be a number that is 2^n
-;          (128, 256, 512, 1024, 2048, 4096, 8192). The number you input isn't
-;          necessarily the final value the audio API decides to use.
+;          (128, 256, 512, 1024, 2048, 4096, 8192, ...). The number you input
+;          isn't necessarily the actual value the audio API decides to use.
 ;          Lower means less audio latency but possible audio issues, higher
-;          means more audio latency but less chance for issues. This will also
-;          change the latency of the VU-meters, spectrum analyzer and scopes.
+;          means more audio latency but less chance for issues.
 ;
 BUFFERSIZE=1024
 
@@ -231,15 +244,12 @@
 ; Amiga 500 low-pass filter (not the "LED" filter)
 ;        Syntax: TRUE or FALSE
 ; Default value: FALSE
-;       Comment: Use a low-pass filter to prevent some
-;         of the aliasing in the sound at the expense of
-;         sound sharpness.
-;         Every Amiga had a low-pass filter like this. All of them except
-;         for Amiga 1200 (~28..31kHz) had it set to something around
-;         4kHz to 5kHz (~4.4kHz).
-;         This must not be confused with the LED filter which can be turned
-;         on/off in software-- the low-pass filter is always enabled and
-;         can't be turned off.
+;       Comment: Enabling this will simulate the ~4421Hz 6dB/oct RC low-pass
+;         filter present in almost all Amigas. This will make the sound a bit
+;         muddier. On Amiga 1200, the cut-off is ~34kHz (sharper sound). This
+;         can also be toggled in the tracker by pressing F12. This must not be
+;         confused with the "LED" filter which can be toggled with the pattern
+;         command E0x.
 ;
 A500LOWPASSFILTER=FALSE
 
--- a/release/other/protracker.ini
+++ b/release/other/protracker.ini
@@ -79,8 +79,6 @@
 ;
 HWMOUSE=TRUE
 
-
-
 [GENERAL SETTINGS]
 ; Hide last modification dates in Disk Op. to get longer dir/file names
 ;        Syntax: TRUE or FALSE
@@ -198,12 +196,10 @@
 ;
 MODDOT=FALSE
 
-; Dotted line in center of sample data view
+; Draw a center line in the SAMPLER screen's waveform
 ;        Syntax: TRUE or FALSE
 ; Default value: TRUE
-;       Comment: Setting it to FALSE will turn off the dotted center line
-;         that is rendered in the middle of the sampler data view in
-;         the sampler screen.
+;       Comment: This used to draw a dotted line, but now draws a line instead
 ;
 DOTTEDCENTER=TRUE
 
@@ -211,20 +207,37 @@
 ; Audio output frequency
 ;        Syntax: Number, in hertz
 ; Default value: 48000
-;       Comment: Ranges from 44100 to 192000.
-;         Also sets the playback frequency for WAVs made with MOD2WAV.
+;       Comment: Ranges from 44100 to 192000. 96000 is recommended if your
+;         OS is set to mix shared audio at 96kHz or higher.
 ;
 FREQUENCY=48000
 
+; Audio input frequency
+;        Syntax: Number, in hertz
+; Default value: 44100
+;       Comment: Ranges from 44100 to 192000. This should be set to match
+;         the frequency used for your audio input device (for sampling).
+;
+SAMPLINGFREQ=44100
+
+; Normalize sampled audio before converting to 8-bit
+;        Syntax: TRUE or FALSE
+; Default value: TRUE
+;       Comment: This one is for the audio sampling feature in the SAMPLER
+;         screen. If it's set to TRUE, it will normalize the gain before it
+;         converts the sample to 8-bit in the end. This will preserve as much
+;         amplitude information as possible to lower quantization noise.
+;
+NORMALIZESAMPLING=TRUE
+
 ; Audio buffer size
 ;        Syntax: Number, in samples
 ; Default value: 1024
 ;       Comment: Ranges from 128 to 8192. Should be a number that is 2^n
-;          (128, 256, 512, 1024, 2048, 4096, 8192). The number you input isn't
-;          necessarily the final value the audio API decides to use.
+;          (128, 256, 512, 1024, 2048, 4096, 8192, ...). The number you input
+;          isn't necessarily the actual value the audio API decides to use.
 ;          Lower means less audio latency but possible audio issues, higher
-;          means more audio latency but less chance for issues. This will also
-;          change the latency of the VU-meters, spectrum analyzer and scopes.
+;          means more audio latency but less chance for issues.
 ;
 BUFFERSIZE=1024
 
@@ -231,15 +244,12 @@
 ; Amiga 500 low-pass filter (not the "LED" filter)
 ;        Syntax: TRUE or FALSE
 ; Default value: FALSE
-;       Comment: Use a low-pass filter to prevent some
-;         of the aliasing in the sound at the expense of
-;         sound sharpness.
-;         Every Amiga had a low-pass filter like this. All of them except
-;         for Amiga 1200 (~28..31kHz) had it set to something around
-;         4kHz to 5kHz (~4.4kHz).
-;         This must not be confused with the LED filter which can be turned
-;         on/off in software-- the low-pass filter is always enabled and
-;         can't be turned off.
+;       Comment: Enabling this will simulate the ~4421Hz 6dB/oct RC low-pass
+;         filter present in almost all Amigas. This will make the sound a bit
+;         muddier. On Amiga 1200, the cut-off is ~34kHz (sharper sound). This
+;         can also be toggled in the tracker by pressing F12. This must not be
+;         confused with the "LED" filter which can be toggled with the pattern
+;         command E0x.
 ;
 A500LOWPASSFILTER=FALSE
 
--- a/release/win32/protracker.ini
+++ b/release/win32/protracker.ini
@@ -79,8 +79,6 @@
 ;
 HWMOUSE=TRUE
 
-
-
 [GENERAL SETTINGS]
 ; Hide last modification dates in Disk Op. to get longer dir/file names
 ;        Syntax: TRUE or FALSE
@@ -198,12 +196,10 @@
 ;
 MODDOT=FALSE
 
-; Dotted line in center of sample data view
+; Draw a center line in the SAMPLER screen's waveform
 ;        Syntax: TRUE or FALSE
 ; Default value: TRUE
-;       Comment: Setting it to FALSE will turn off the dotted center line
-;         that is rendered in the middle of the sampler data view in
-;         the sampler screen.
+;       Comment: This used to draw a dotted line, but now draws a line instead
 ;
 DOTTEDCENTER=TRUE
 
@@ -211,20 +207,37 @@
 ; Audio output frequency
 ;        Syntax: Number, in hertz
 ; Default value: 48000
-;       Comment: Ranges from 44100 to 192000.
-;         Also sets the playback frequency for WAVs made with MOD2WAV.
+;       Comment: Ranges from 44100 to 192000. 96000 is recommended if your
+;         OS is set to mix shared audio at 96kHz or higher.
 ;
 FREQUENCY=48000
 
+; Audio input frequency
+;        Syntax: Number, in hertz
+; Default value: 44100
+;       Comment: Ranges from 44100 to 192000. This should be set to match
+;         the frequency used for your audio input device (for sampling).
+;
+SAMPLINGFREQ=44100
+
+; Normalize sampled audio before converting to 8-bit
+;        Syntax: TRUE or FALSE
+; Default value: TRUE
+;       Comment: This one is for the audio sampling feature in the SAMPLER
+;         screen. If it's set to TRUE, it will normalize the gain before it
+;         converts the sample to 8-bit in the end. This will preserve as much
+;         amplitude information as possible to lower quantization noise.
+;
+NORMALIZESAMPLING=TRUE
+
 ; Audio buffer size
 ;        Syntax: Number, in samples
 ; Default value: 1024
 ;       Comment: Ranges from 128 to 8192. Should be a number that is 2^n
-;          (128, 256, 512, 1024, 2048, 4096, 8192). The number you input isn't
-;          necessarily the final value the audio API decides to use.
+;          (128, 256, 512, 1024, 2048, 4096, 8192, ...). The number you input
+;          isn't necessarily the actual value the audio API decides to use.
 ;          Lower means less audio latency but possible audio issues, higher
-;          means more audio latency but less chance for issues. This will also
-;          change the latency of the VU-meters, spectrum analyzer and scopes.
+;          means more audio latency but less chance for issues.
 ;
 BUFFERSIZE=1024
 
@@ -231,15 +244,12 @@
 ; Amiga 500 low-pass filter (not the "LED" filter)
 ;        Syntax: TRUE or FALSE
 ; Default value: FALSE
-;       Comment: Use a low-pass filter to prevent some
-;         of the aliasing in the sound at the expense of
-;         sound sharpness.
-;         Every Amiga had a low-pass filter like this. All of them except
-;         for Amiga 1200 (~28..31kHz) had it set to something around
-;         4kHz to 5kHz (~4.4kHz).
-;         This must not be confused with the LED filter which can be turned
-;         on/off in software-- the low-pass filter is always enabled and
-;         can't be turned off.
+;       Comment: Enabling this will simulate the ~4421Hz 6dB/oct RC low-pass
+;         filter present in almost all Amigas. This will make the sound a bit
+;         muddier. On Amiga 1200, the cut-off is ~34kHz (sharper sound). This
+;         can also be toggled in the tracker by pressing F12. This must not be
+;         confused with the "LED" filter which can be toggled with the pattern
+;         command E0x.
 ;
 A500LOWPASSFILTER=FALSE
 
--- a/release/win64/protracker.ini
+++ b/release/win64/protracker.ini
@@ -79,8 +79,6 @@
 ;
 HWMOUSE=TRUE
 
-
-
 [GENERAL SETTINGS]
 ; Hide last modification dates in Disk Op. to get longer dir/file names
 ;        Syntax: TRUE or FALSE
@@ -198,12 +196,10 @@
 ;
 MODDOT=FALSE
 
-; Dotted line in center of sample data view
+; Draw a center line in the SAMPLER screen's waveform
 ;        Syntax: TRUE or FALSE
 ; Default value: TRUE
-;       Comment: Setting it to FALSE will turn off the dotted center line
-;         that is rendered in the middle of the sampler data view in
-;         the sampler screen.
+;       Comment: This used to draw a dotted line, but now draws a line instead
 ;
 DOTTEDCENTER=TRUE
 
@@ -211,20 +207,37 @@
 ; Audio output frequency
 ;        Syntax: Number, in hertz
 ; Default value: 48000
-;       Comment: Ranges from 44100 to 192000.
-;         Also sets the playback frequency for WAVs made with MOD2WAV.
+;       Comment: Ranges from 44100 to 192000. 96000 is recommended if your
+;         OS is set to mix shared audio at 96kHz or higher.
 ;
 FREQUENCY=48000
 
+; Audio input frequency
+;        Syntax: Number, in hertz
+; Default value: 44100
+;       Comment: Ranges from 44100 to 192000. This should be set to match
+;         the frequency used for your audio input device (for sampling).
+;
+SAMPLINGFREQ=44100
+
+; Normalize sampled audio before converting to 8-bit
+;        Syntax: TRUE or FALSE
+; Default value: TRUE
+;       Comment: This one is for the audio sampling feature in the SAMPLER
+;         screen. If it's set to TRUE, it will normalize the gain before it
+;         converts the sample to 8-bit in the end. This will preserve as much
+;         amplitude information as possible to lower quantization noise.
+;
+NORMALIZESAMPLING=TRUE
+
 ; Audio buffer size
 ;        Syntax: Number, in samples
 ; Default value: 1024
 ;       Comment: Ranges from 128 to 8192. Should be a number that is 2^n
-;          (128, 256, 512, 1024, 2048, 4096, 8192). The number you input isn't
-;          necessarily the final value the audio API decides to use.
+;          (128, 256, 512, 1024, 2048, 4096, 8192, ...). The number you input
+;          isn't necessarily the actual value the audio API decides to use.
 ;          Lower means less audio latency but possible audio issues, higher
-;          means more audio latency but less chance for issues. This will also
-;          change the latency of the VU-meters, spectrum analyzer and scopes.
+;          means more audio latency but less chance for issues.
 ;
 BUFFERSIZE=1024
 
@@ -231,15 +244,12 @@
 ; Amiga 500 low-pass filter (not the "LED" filter)
 ;        Syntax: TRUE or FALSE
 ; Default value: FALSE
-;       Comment: Use a low-pass filter to prevent some
-;         of the aliasing in the sound at the expense of
-;         sound sharpness.
-;         Every Amiga had a low-pass filter like this. All of them except
-;         for Amiga 1200 (~28..31kHz) had it set to something around
-;         4kHz to 5kHz (~4.4kHz).
-;         This must not be confused with the LED filter which can be turned
-;         on/off in software-- the low-pass filter is always enabled and
-;         can't be turned off.
+;       Comment: Enabling this will simulate the ~4421Hz 6dB/oct RC low-pass
+;         filter present in almost all Amigas. This will make the sound a bit
+;         muddier. On Amiga 1200, the cut-off is ~34kHz (sharper sound). This
+;         can also be toggled in the tracker by pressing F12. This must not be
+;         confused with the "LED" filter which can be toggled with the pattern
+;         command E0x.
 ;
 A500LOWPASSFILTER=FALSE
 
binary files a/src/gfx/bmp/font.bmp b/src/gfx/bmp/font.bmp differ
binary files /dev/null b/src/gfx/bmp/samplemonitor.bmp differ
binary files a/src/gfx/bmp/sampler.bmp b/src/gfx/bmp/sampler.bmp differ
binary files /dev/null b/src/gfx/bmp/samplingbox.bmp differ
--- a/src/gfx/pt2_gfx_font.c
+++ b/src/gfx/pt2_gfx_font.c
@@ -2,12 +2,12 @@
 
 const uint8_t fontBMP[6096] =
 {
-	0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,
+	0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,
+	0,0,0,0,1,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,1,1,0,0,1,1,0,
 	0,1,1,0,1,1,0,0,0,1,1,1,1,1,1,0,0,1,1,0,0,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,1,1,0,0,
 	0,0,0,0,1,1,0,0,0,0,0,1,1,0,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,
@@ -28,7 +28,7 @@
 	0,0,1,1,1,1,1,0,0,1,1,1,1,1,1,0,0,1,1,0,0,1,1,0,0,1,1,0,0,1,1,0,0,1,1,0,0,1,1,0,
 	0,1,1,0,0,1,1,0,0,1,1,0,0,1,1,0,0,1,1,1,1,1,1,0,0,0,0,1,1,1,0,0,0,0,0,1,1,0,0,0,
 	0,0,1,1,1,0,0,0,0,0,1,1,1,0,1,1,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,
-	0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+	0,1,1,1,1,1,0,0,0,0,1,1,1,1,1,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
@@ -53,12 +53,12 @@
 	0,1,1,0,0,1,1,0,0,1,1,0,0,1,1,0,0,1,1,0,0,0,0,0,0,0,0,1,1,0,0,0,0,1,1,0,0,1,1,0,
 	0,1,1,0,0,1,1,0,0,1,1,0,0,1,1,0,0,0,1,1,1,1,0,0,0,1,1,0,0,1,1,0,0,0,0,0,1,1,0,0,
 	0,0,1,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,1,0,0,0,1,1,0,1,1,1,0,0,0,0,0,0,0,0,0,
-	0,1,1,1,1,0,0,0,0,0,0,1,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+	0,1,1,1,1,0,0,0,0,0,0,1,0,0,0,0,0,1,1,1,1,1,1,0,0,1,1,1,1,1,1,0,0,0,1,1,1,1,1,0,
+	0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,1,1,0,0,
 	0,1,1,1,1,1,0,0,0,0,0,1,1,0,0,0,0,0,1,1,1,0,1,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,
 	0,0,0,0,1,1,0,0,0,1,1,1,1,1,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0,
@@ -79,12 +79,12 @@
 	0,0,0,1,1,0,0,0,0,1,1,0,0,1,1,0,0,1,1,0,0,1,1,0,0,1,1,1,1,1,1,0,0,0,0,1,1,0,0,0,
 	0,0,1,1,1,1,0,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,1,1,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,1,1,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0,
+	0,0,1,1,1,1,1,0,0,0,0,1,1,1,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,1,0,1,1,0,0,0,1,1,0,1,1,0,0,1,0,0,0,1,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,1,1,0,0,0,0,1,1,1,0,0,0,0,0,0,1,0,0,0,0,
 	0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,1,1,0,0,0,0,0,1,1,0,0,1,1,0,
@@ -104,7 +104,7 @@
 	0,1,1,0,1,1,0,0,0,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,1,1,0,0,0,1,1,1,1,0,0,
 	0,1,1,0,0,1,1,0,0,0,1,1,1,1,0,0,0,0,0,1,1,0,0,0,0,0,1,1,0,0,0,0,0,0,1,0,0,0,0,0,
 	0,0,0,1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,
-	0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+	0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
@@ -130,6 +130,7 @@
 	0,0,1,1,1,1,0,0,0,0,0,1,1,0,0,0,0,1,0,0,0,0,1,0,0,1,1,0,0,1,1,0,0,0,0,1,1,0,0,0,
 	0,1,1,1,1,1,1,0,0,0,0,1,1,1,0,0,0,0,0,1,1,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+	0,0,0,1,1,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
@@ -153,6 +154,5 @@
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 };
--- a/src/gfx/pt2_gfx_sampler.c
+++ b/src/gfx/pt2_gfx_sampler.c
@@ -13,6 +13,110 @@
 	0x00FFFF,0x00FFFF,0x00FFFF,0x00FFFF,0x00FFFF,0x00FFFF,0x00FFFF,0x00FFFF
 };
 
+// Final unpack length: 11000
+// Decoded length: 2750 (first four bytes of buffer)
+const uint8_t sampleMonitorPackedBMP[441] =
+{
+	0x00,0x00,0x0A,0xBE,0xCC,0x30,0x55,0x56,0x6A,0xCC,0x2F,0xAA,0xAB,0x6A,0xCC,0x2F,0xAA,0xAB,0x6A,0xCC,
+	0x0B,0xAA,0x55,0x69,0x56,0x9A,0xA6,0x55,0x69,0x6A,0xA5,0x55,0xAA,0xAA,0x6A,0x9A,0x55,0xA5,0xA5,0xA5,
+	0x5A,0x55,0x5A,0x55,0xA5,0x56,0xCC,0x0B,0xAA,0xAB,0x6A,0xCC,0x0A,0xAA,0xA9,0x7F,0xF5,0xF5,0x96,0x97,
+	0x5F,0x59,0x7A,0xA5,0xFF,0xEA,0xAA,0x5A,0x5D,0x7D,0x65,0x65,0xE9,0x7E,0xB5,0xFD,0x7D,0x65,0xF5,0xCC,
+	0x0B,0xAA,0xAB,0x6A,0xCC,0x0B,0xAA,0x55,0xA5,0x55,0xD5,0x57,0x55,0x7D,0x7A,0xA5,0x5A,0xAA,0xAA,0x55,
+	0x5D,0x79,0x75,0x55,0xE9,0x7A,0xA5,0xE9,0x79,0x75,0x57,0xEA,0xCC,0x0A,0xAA,0xAB,0x6A,0xCC,0x0B,0xAA,
+	0xBD,0x65,0xF5,0xD7,0xD7,0x5F,0xF9,0x7A,0xA5,0xFE,0xAA,0xAA,0x5F,0x5D,0x79,0x75,0xD5,0xE9,0x7A,0xA5,
+	0xE9,0x79,0x75,0xD7,0xCC,0x0B,0xAA,0xAB,0x6A,0xCC,0x0A,0xAA,0xA9,0x55,0xF5,0xE5,0xD7,0x97,0x5E,0xA9,
+	0x55,0x65,0x55,0xAA,0xAA,0x5E,0x5E,0x55,0xF5,0xE5,0xE5,0x5A,0xA5,0xEA,0x55,0xF5,0xE5,0xCC,0x0B,0xAA,
+	0xAB,0x6A,0xCC,0x0B,0xAA,0xFF,0xEB,0xEB,0xEF,0xAF,0xBE,0xAA,0xFF,0xFB,0xFF,0xEA,0xAA,0xBE,0xBE,0xBF,
+	0xEB,0xEB,0xEB,0xFE,0xAB,0xEA,0xBF,0xEB,0xEB,0xEA,0xCC,0x0A,0xAA,0xAB,0x6A,0xCC,0x2F,0xAA,0xAB,0xBF,
+	0xCC,0x30,0xFF,0xCC,0x30,0x55,0x56,0x6A,0xCC,0x2F,0xAA,0xAB,0x6F,0xCC,0x2F,0xFF,0xEB,0x6C,0xCC,0x2F,
+	0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,
+	0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,
+	0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,
+	0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,
+	0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x7C,0xCC,0x2F,0x00,0x17,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,
+	0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,
+	0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,
+	0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,
+	0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x6C,0xCC,0x2F,
+	0x00,0x1B,0x6C,0xCC,0x2F,0x00,0x1B,0x69,0xCC,0x2F,0x55,0x5B,0x6A,0xCC,0x2F,0xAA,0xAB,0xBF,0xCC,0x30,
+	0xFF
+};
+
+// Final unpack length: 16640
+// Decoded length: 4160 (first four bytes of buffer)
+const uint8_t samplingBoxPackedBMP[1379] =
+{
+	0x00,0x00,0x10,0x40,0x6A,0xCC,0x4D,0xAA,0xAB,0x6A,0xCC,0x4D,0xAA,0xAB,0x6A,0xCC,0x21,0xAA,0xA9,0xCC,
+	0x07,0x55,0xCC,0x22,0xAA,0xAB,0x65,0x55,0x56,0xCC,0x08,0xAA,0x95,0x55,0x5A,0xCC,0x09,0xAA,0xA9,0x55,
+	0x55,0xCC,0x06,0xAA,0xA9,0xCC,0x07,0xAA,0xEA,0xCC,0x21,0xAA,0xAB,0x66,0xAA,0xAB,0xCC,0x08,0xAA,0x9A,
+	0xAA,0xAE,0xCC,0x09,0xAA,0xA9,0xAA,0xAA,0xEA,0xCC,0x05,0xAA,0xA9,0xCC,0x07,0xAA,0xEA,0xCC,0x21,0xAA,
+	0xAB,0x66,0xAA,0xAB,0xA5,0xAA,0x95,0x56,0x55,0x59,0x55,0x6A,0xAA,0x9A,0xAA,0xAE,0x95,0x5A,0x95,0x6A,
+	0x55,0x65,0xA5,0x95,0x56,0xAA,0xA9,0xAA,0xAA,0xE9,0xAA,0x69,0x56,0x96,0x96,0xAA,0xA9,0xA9,0x55,0xA5,
+	0x5A,0x95,0x69,0x69,0x6A,0xEA,0xA5,0x55,0x95,0x5A,0x55,0x5A,0x55,0xA5,0xA5,0x95,0x56,0x5A,0x5A,0x55,
+	0xA5,0xA5,0xCC,0x11,0xAA,0xAB,0x66,0xAA,0xAB,0xA5,0xEA,0x97,0xFF,0x5F,0xFE,0xD7,0xFA,0xAA,0x9A,0xAA,
+	0xAE,0x97,0xD6,0xA5,0xF9,0x7F,0xF5,0xE5,0xED,0x7F,0xAA,0xA9,0xAA,0xAA,0xE9,0x69,0x7A,0x5F,0xA5,0x5F,
+	0xAA,0xA9,0xA5,0xFF,0xD7,0xD6,0x5F,0x59,0x59,0x7A,0xEA,0xA5,0xFF,0xD7,0xD6,0x5F,0xFD,0x7D,0x65,0xE5,
+	0xD7,0xFF,0x56,0x5D,0x7D,0x65,0xE5,0xEA,0xCC,0x10,0xAA,0xAB,0x66,0xAA,0xAB,0xA5,0xEA,0x95,0x6A,0x55,
+	0xAA,0x97,0xAA,0xAA,0x9A,0xAA,0xAE,0x95,0x5F,0xA5,0xE9,0x75,0x65,0x55,0xE9,0x7A,0xAA,0xA9,0xAA,0xAA,
+	0xE9,0x55,0x7A,0x5E,0xA9,0x7E,0xAA,0xA9,0xA9,0x56,0x97,0xAF,0x55,0x5D,0x55,0x7A,0xEA,0xA5,0x5A,0x95,
+	0x5F,0x55,0xA9,0x79,0x75,0xE5,0xD5,0x6A,0x55,0x5D,0x7A,0xF9,0x57,0xEA,0xCC,0x10,0xAA,0xAB,0x66,0xAA,
+	0xAB,0xA5,0xEA,0x97,0xFA,0x5F,0xEA,0x97,0xAA,0xAA,0x9A,0xAA,0xAE,0x97,0x5E,0xA5,0xE9,0x79,0x75,0xF5,
+	0xE9,0x7A,0xAA,0xA9,0xAA,0xAA,0xE9,0x7D,0x7A,0x5E,0xA5,0x5A,0xAA,0xA9,0xAA,0xF5,0x97,0x96,0x5F,0x5D,
+	0x75,0x7A,0xEA,0xA5,0xFE,0x97,0x5E,0x5F,0xE9,0x75,0xF5,0xE5,0xD7,0xFA,0x5D,0x5D,0x79,0x6A,0x5F,0xCC,
+	0x11,0xAA,0xAB,0x66,0xAA,0xAB,0xA5,0x55,0x95,0x56,0x5E,0xAA,0x97,0xAA,0xAA,0x9A,0xAA,0xAE,0x97,0x96,
+	0x95,0x6A,0x55,0x75,0xE5,0xE9,0x7A,0xAA,0xA9,0xAA,0xAA,0xE9,0x79,0x79,0x56,0x97,0xD6,0xAA,0xA9,0xA5,
+	0x57,0xE5,0x5F,0x5E,0x5D,0x79,0x7A,0xEA,0xA5,0xEA,0x97,0x96,0x55,0x5A,0x59,0x69,0x57,0xD5,0x56,0x5E,
+	0x5E,0x55,0xFA,0x5E,0xCC,0x11,0xAA,0xAB,0x66,0xAA,0xAB,0xAB,0xFF,0xEF,0xFF,0xBE,0xAA,0xAF,0xAA,0xAA,
+	0x9A,0xAA,0xAE,0xAF,0xAF,0xAF,0xFA,0xBF,0xFB,0xEB,0xEA,0xFA,0xAA,0xA9,0xAA,0xAA,0xEA,0xFA,0xFA,0xFF,
+	0xAF,0xAF,0xAA,0xA9,0xAB,0xFF,0xAB,0xFE,0xBE,0xBE,0xFA,0xFA,0xEA,0xAB,0xEA,0xAF,0xAF,0xBF,0xFE,0xBE,
+	0xFA,0xFF,0xAF,0xFF,0xBE,0xBE,0xBF,0xEA,0xBE,0xCC,0x11,0xAA,0xAB,0x6B,0xFF,0xFF,0xCC,0x08,0xAA,0xAF,
+	0xFF,0xFE,0xCC,0x0A,0xAA,0xFF,0xFF,0xEA,0xCC,0x05,0xAA,0xA9,0xCC,0x07,0xAA,0xEA,0xCC,0x21,0xAA,0xAB,
+	0x6A,0xCC,0x22,0xAA,0xCC,0x07,0xFF,0xEA,0xCC,0x21,0xAA,0xAB,0x6A,0xCC,0x4D,0xAA,0xAB,0x6A,0xCC,0x48,
+	0xAA,0x55,0x55,0x59,0x55,0x55,0x6B,0x6F,0xCC,0x27,0xFF,0xF5,0x55,0x55,0xCC,0x1D,0xAA,0x6A,0xAA,0xAD,
+	0xAA,0xAA,0xBB,0x6C,0xCC,0x27,0x00,0x06,0xAA,0xAA,0xEA,0xA5,0x55,0xA5,0x5A,0x5A,0x59,0x55,0x65,0x55,
+	0x96,0x96,0x5A,0x59,0x55,0x6A,0xCC,0x0D,0xAA,0x6A,0x9A,0xAD,0xA9,0x5A,0xBB,0x6C,0xCC,0x27,0x00,0x06,
+	0xA9,0xAA,0xEA,0xA5,0xFF,0xE9,0x7E,0x56,0x5D,0x7F,0xFB,0x5F,0xD7,0x97,0x56,0x5D,0x7F,0xFA,0xCC,0x0D,
+	0xAA,0x6A,0x56,0xAD,0xA9,0x5E,0xBB,0x6C,0xCC,0x27,0x00,0x06,0xA5,0x6A,0xEA,0xA5,0x5A,0xA9,0x7A,0x55,
+	0x5D,0x56,0xAA,0x5E,0x97,0x97,0x55,0x5D,0x56,0xCC,0x0E,0xAA,0x69,0x55,0xAD,0xA9,0x5E,0xBB,0x6C,0xCC,
+	0x27,0x00,0x06,0x95,0x5A,0xEA,0xA5,0xFE,0xA9,0x7A,0x5D,0x5D,0x7F,0xAA,0x5E,0x97,0x97,0x5D,0x5D,0x7F,
+	0xCC,0x0E,0xAA,0x6A,0x57,0xED,0xA5,0x56,0xBB,0x6C,0xCC,0x27,0x00,0x06,0xA5,0x7E,0xEA,0xA5,0xEA,0xA5,
+	0x5A,0x5E,0x5D,0x55,0x6A,0x5E,0xA5,0x5F,0x5E,0x5D,0x55,0x6A,0xCC,0x0D,0xAA,0x6A,0x57,0xAD,0xA9,0x5F,
+	0xBB,0x6C,0xCC,0x27,0x00,0x06,0xA5,0x7A,0xEA,0xAB,0xEA,0xAB,0xFE,0xBE,0xBE,0xFF,0xFA,0xBE,0xAB,0xFE,
+	0xBE,0xBE,0xFF,0xFA,0xCC,0x0D,0xAA,0x6A,0x57,0xAD,0xAA,0x7E,0xBB,0x6C,0xCC,0x27,0x00,0x06,0xA5,0x7A,
+	0xEA,0xCC,0x1C,0xAA,0x6A,0xBF,0xAD,0xAA,0xBA,0xBB,0x6C,0xCC,0x27,0x00,0x06,0xAB,0xFA,0xEA,0xCC,0x1C,
+	0xAA,0x6A,0xAA,0xAD,0xAA,0xAA,0xBB,0x6C,0xCC,0x27,0x00,0x06,0xAA,0xAA,0xEA,0xCC,0x1C,0xAA,0xBF,0xFF,
+	0xFE,0xFF,0xFF,0xFB,0x6C,0xCC,0x27,0x00,0x0B,0xFF,0xFF,0xEA,0xCC,0x21,0xAA,0xAB,0x6C,0xCC,0x27,0x00,
+	0x05,0x55,0x55,0xCC,0x22,0xAA,0xAB,0x6C,0xCC,0x27,0x00,0x06,0xAA,0xAA,0xEA,0xA5,0xA5,0xA5,0x5A,0x55,
+	0x59,0x55,0x6A,0xCC,0x19,0xAA,0xAB,0x6C,0xCC,0x27,0x00,0x06,0xAA,0xAA,0xEA,0xA5,0x65,0xD7,0xD6,0xB5,
+	0xFD,0x7F,0xFA,0xCC,0x19,0xAA,0xAB,0x6C,0xCC,0x27,0x00,0x06,0xAA,0xAA,0xEA,0xA5,0x55,0xD7,0x97,0xA5,
+	0xE9,0x56,0xCC,0x1A,0xAA,0xAB,0x6C,0xCC,0x27,0x00,0x06,0xAA,0xAA,0xEA,0xA5,0xD5,0xD7,0x97,0xA5,0xE9,
+	0x7F,0xCC,0x1A,0xAA,0xAB,0x6C,0xCC,0x27,0x00,0x06,0xAA,0xAA,0xEA,0xA5,0xE5,0xE5,0x5F,0xA5,0xE9,0x55,
+	0x6A,0xCC,0x19,0xAA,0xAB,0x6C,0xCC,0x27,0x00,0x06,0xAA,0xAA,0xEA,0xAB,0xEB,0xEB,0xFE,0xAB,0xEA,0xFF,
+	0xFA,0xCC,0x19,0xAA,0xAB,0x6C,0xCC,0x27,0x00,0x06,0xAA,0xAA,0xEA,0xCC,0x21,0xAA,0xAB,0x6C,0xCC,0x27,
+	0x00,0x06,0xAA,0xAA,0xEA,0xCC,0x21,0xAA,0xAB,0x6C,0xCC,0x27,0x00,0x06,0xAA,0xAA,0xEA,0xCC,0x21,0xAA,
+	0xAB,0x6C,0xCC,0x27,0x00,0x06,0xAA,0xAA,0xEA,0xCC,0x21,0xAA,0xAB,0x6C,0xCC,0x27,0x00,0x06,0xAA,0xAA,
+	0xEA,0xCC,0x21,0xAA,0xAB,0x6C,0xCC,0x27,0x00,0x0B,0xFF,0xFF,0xEA,0xCC,0x21,0xAA,0xAB,0x6C,0xCC,0x27,
+	0x00,0x05,0x55,0x55,0xAA,0x95,0xCC,0x0C,0x55,0x6A,0xA5,0xCC,0x07,0x55,0x6A,0xA5,0xCC,0x07,0x55,0x6B,
+	0x6C,0xCC,0x27,0x00,0x06,0xAA,0xAA,0xEA,0x9A,0xCC,0x0C,0xAA,0xBA,0xA6,0xCC,0x07,0xAA,0xBA,0xA6,0xCC,
+	0x07,0xAA,0xBB,0x6C,0xCC,0x27,0x00,0x06,0xA5,0x6A,0xEA,0x9A,0xCC,0x0C,0xAA,0xBA,0xA6,0xCC,0x07,0xAA,
+	0xBA,0xA6,0xCC,0x07,0xAA,0xBB,0x6C,0xCC,0x27,0x00,0x06,0xA5,0x7A,0xEA,0x9A,0xAA,0x95,0x5A,0x55,0xA6,
+	0xA9,0x95,0x5A,0x5A,0xA9,0x55,0x6A,0xAA,0xBA,0xA6,0xA5,0xA5,0xA5,0x5A,0x55,0x59,0x55,0x6A,0xBA,0xA6,
+	0xA5,0x55,0x96,0x96,0x95,0x69,0x55,0x6A,0xBB,0x6C,0xCC,0x27,0x00,0x06,0xA5,0x7A,0xEA,0x9A,0xAA,0x5F,
+	0xFD,0x7D,0x65,0xA5,0xD7,0xD6,0x5E,0xA9,0x7F,0xFA,0xAA,0xBA,0xA6,0xA5,0x65,0xD7,0xD6,0xB5,0xFD,0x7F,
+	0xFA,0xBA,0xA6,0xA5,0xFF,0xE5,0x5F,0xA5,0xFA,0xD7,0xFA,0xBB,0x6C,0xCC,0x27,0x00,0x06,0x95,0x5A,0xEA,
+	0x9A,0xAA,0x95,0x69,0x55,0x75,0x55,0xD5,0x5F,0x5E,0xA9,0x56,0xAA,0xAA,0xBA,0xA6,0xA5,0x55,0xD7,0x97,
+	0xA5,0xE9,0x56,0xAA,0xBA,0xA6,0xA5,0x5A,0xA9,0x7E,0xA5,0xEA,0x97,0xAA,0xBB,0x6C,0xCC,0x27,0x00,0x06,
+	0xA5,0x7E,0xEA,0x9A,0xAA,0xAF,0x59,0x7D,0x75,0xF5,0xD7,0xFE,0x5E,0xA9,0x7F,0xAA,0xAA,0xBA,0xA6,0xA5,
+	0xD5,0xD7,0x97,0xA5,0xE9,0x7F,0xAA,0xBA,0xA6,0xA5,0xFE,0xA5,0x5A,0xA5,0xEA,0x97,0xAA,0xBB,0x6C,0xCC,
+	0x27,0x00,0x06,0xA9,0xFA,0xEA,0x9A,0xAA,0x55,0x7D,0x79,0x75,0xE5,0xD7,0xAA,0x55,0x59,0x55,0x6A,0xAA,
+	0xBA,0xA6,0xA5,0xE5,0xE5,0x5F,0xA5,0xE9,0x55,0x6A,0xBA,0xA6,0xA5,0x55,0x97,0xD6,0x95,0x6A,0x97,0xAA,
+	0xBB,0x6C,0xCC,0x27,0x00,0x06,0xAA,0xEA,0xEA,0x9A,0xAA,0xBF,0xFA,0xFA,0xFB,0xEB,0xEF,0xAA,0xBF,0xFE,
+	0xFF,0xFA,0xAA,0xBA,0xA6,0xAB,0xEB,0xEB,0xFE,0xAB,0xEA,0xFF,0xFA,0xBA,0xA6,0xAB,0xFF,0xEF,0xAF,0xAF,
+	0xFA,0xAF,0xAA,0xBB,0x6C,0xCC,0x27,0x00,0x06,0xAA,0xAA,0xEA,0x9A,0xCC,0x0C,0xAA,0xBA,0xA6,0xCC,0x07,
+	0xAA,0xBA,0xA6,0xCC,0x07,0xAA,0xBB,0x69,0xCC,0x27,0x55,0x5B,0xFF,0xFF,0xEA,0xAF,0xCC,0x0C,0xFF,0xFA,
+	0xAB,0xCC,0x07,0xFF,0xFA,0xAB,0xCC,0x07,0xFF,0xFB,0x6A,0xCC,0x4D,0xAA,0xAB,0xBF,0xCC,0x4E,0xFF
+};
+
 // Final unpack length: 4488
 // Decoded length: 1122 (first four bytes of buffer)
 const uint8_t samplerVolumePackedBMP[706] =
@@ -119,7 +223,7 @@
 
 // Final unpack length: 42880
 // Decoded length: 10720 (first four bytes of buffer)
-const uint8_t samplerScreenPackedBMP[3056] =
+const uint8_t samplerScreenPackedBMP[3076] =
 {
 	0x00,0x00,0x29,0xE0,0xCC,0x4E,0x55,0x56,0x6A,0xCC,0x4D,0xAA,0xAB,0x6A,0xCC,0x37,0xAA,0xA5,0x55,0x96,
 	0x96,0x5A,0x59,0x55,0x6A,0xCC,0x0D,0xAA,0xAB,0x6A,0xA5,0xCC,0x03,0x55,0x6A,0xCC,0x31,0xAA,0xAB,0x5F,
@@ -213,36 +317,37 @@
 	0xED,0x7F,0x5F,0x59,0x7D,0x6B,0x6A,0xCC,0x0D,0xAA,0xAB,0x6A,0xCC,0x11,0xAA,0xAB,0x6A,0xCC,0x0F,0xAA,
 	0xB6,0xCC,0x10,0xAA,0xAB,0x69,0x56,0xA9,0x7A,0x5E,0x5D,0x55,0xFB,0xBF,0xCC,0x0E,0xFF,0xBF,0xCC,0x12,
 	0xFF,0xBF,0xCC,0x0F,0xFF,0xFB,0xCC,0x11,0xFF,0x6A,0xF5,0xA9,0x7A,0x5E,0x5D,0x7F,0xEB,0xCC,0x0E,0x55,
-	0x56,0xCC,0x03,0x55,0x56,0xCC,0x03,0x55,0x56,0xCC,0x08,0x55,0x56,0xCC,0x10,0x55,0x65,0xCC,0x10,0x55,
-	0x56,0x65,0x57,0xE9,0x7A,0x95,0x7D,0x7A,0xAB,0x6A,0xCC,0x0D,0xAA,0xAB,0x6A,0xAA,0xAA,0xAA,0xAB,0x6A,
-	0xAA,0xAA,0xAA,0xAB,0x6A,0xCC,0x07,0xAA,0xAB,0x6A,0xCC,0x0F,0xAA,0xB6,0xCC,0x10,0xAA,0xAB,0x6B,0xFF,
-	0xAA,0xFA,0xAF,0xFA,0xFA,0xAB,0x6A,0xCC,0x0D,0xAA,0xAB,0x6A,0xAA,0xAA,0xAA,0xAB,0x6A,0xAA,0xAA,0xAA,
-	0xAB,0x6A,0xCC,0x07,0xAA,0xAB,0x6A,0xCC,0x0F,0xAA,0xB6,0xCC,0x10,0xAA,0xAB,0x6A,0xCC,0x05,0xAA,0xAB,
-	0x6A,0xAA,0xAA,0xA9,0x55,0xA9,0x56,0x96,0x96,0x95,0x59,0x55,0x6A,0xAA,0xAA,0xAB,0x6A,0x5A,0x56,0x56,
-	0xAB,0x6A,0x56,0x5A,0x5A,0xAB,0x6A,0xAA,0x56,0x56,0x5A,0x56,0x56,0x56,0xAA,0xAB,0x69,0x55,0xA5,0x55,
-	0xA5,0x56,0x95,0x69,0xAA,0x65,0x56,0x96,0xAA,0x55,0x5A,0xAA,0x6A,0xB6,0x96,0x96,0x95,0x69,0x55,0x65,
-	0x55,0xA9,0x6A,0xCC,0x07,0xAA,0xAB,0x6A,0xCC,0x05,0xAA,0xAB,0x6A,0xAA,0xAA,0xA9,0x7D,0x65,0xF5,0x95,
-	0x97,0x5F,0xFD,0x7F,0xFA,0xAA,0xAA,0xAB,0x6A,0x76,0x7F,0x7F,0xAB,0x6A,0x7F,0x76,0x76,0xAB,0x6A,0xAA,
-	0x7F,0x7F,0x76,0x9F,0x7F,0x77,0xAA,0xAB,0x69,0x7D,0x65,0xFF,0xD7,0xFF,0x5F,0x59,0x69,0x75,0xF5,0x97,
-	0xAA,0x5F,0xFE,0x95,0x5A,0xB6,0x95,0x97,0x5F,0x5A,0xD7,0xF5,0xFF,0xE9,0x7A,0xCC,0x07,0xAA,0xAB,0x6A,
-	0xCC,0x05,0xAA,0xAB,0x6A,0xAA,0xAA,0xA9,0x55,0xF5,0x55,0xD5,0x57,0x5D,0x59,0x56,0xAA,0xAA,0xAA,0xAB,
-	0x6A,0x5B,0x5A,0x76,0xAB,0x6A,0x5A,0x77,0x77,0xAB,0x6A,0xAA,0x7A,0x5A,0x77,0x9E,0x5A,0x5B,0xAA,0xAB,
-	0x69,0x55,0xF5,0x5A,0xA5,0x5A,0x55,0x5D,0x55,0x75,0x57,0xD7,0xAA,0x55,0xAA,0x95,0x56,0xB6,0x95,0x57,
-	0x5E,0x5E,0x97,0xA5,0x5A,0xAA,0xFA,0xCC,0x07,0xAA,0xAB,0x6A,0xCC,0x05,0xAA,0xAB,0x6A,0xAA,0xAA,0xA9,
-	0x75,0xE5,0xF5,0xD7,0x57,0x5E,0x5D,0x7F,0xAA,0xAA,0xAA,0xAB,0x6A,0x76,0x7E,0x77,0xAB,0x6A,0x7E,0x77,
-	0x77,0xAB,0x6A,0xAA,0x7A,0x7E,0x77,0x9E,0x7E,0x76,0xAA,0xAB,0x69,0x75,0xE5,0xFE,0xAB,0xD6,0x5F,0x5D,
-	0x7D,0x75,0xFF,0x97,0xAA,0x5F,0xEA,0x95,0x5F,0xB6,0x97,0x57,0x5E,0x5E,0x97,0xA5,0xFE,0xA9,0x6A,0xCC,
-	0x07,0xAA,0xAB,0x6A,0xCC,0x05,0xAA,0xAB,0x6A,0xAA,0xAA,0xA9,0x79,0x65,0xE5,0xD7,0x97,0x95,0x5D,0x55,
-	0x6A,0xAA,0xAA,0xAB,0x6A,0x5B,0x56,0x57,0xAB,0x6A,0x56,0x77,0x5B,0xAB,0x6A,0xAA,0x56,0x56,0x77,0x9E,
-	0x56,0x77,0xAA,0xAB,0x69,0x79,0x65,0x55,0x95,0x5F,0x5E,0x5D,0x79,0x75,0xEA,0x95,0x56,0x55,0x5A,0xAF,
-	0x7E,0xB6,0x97,0x97,0x95,0x7E,0x97,0xA5,0x55,0xA9,0x7A,0xCC,0x07,0xAA,0xAB,0x6A,0xCC,0x05,0xAA,0xAB,
-	0x6A,0xAA,0xAA,0xAA,0xFA,0xFB,0xEB,0xEF,0xAF,0xAF,0xFE,0xFF,0xFA,0xAA,0xAA,0xAB,0x6A,0xBE,0xBF,0xBF,
-	0xAB,0x6A,0xBF,0xBB,0xBE,0xAB,0x6A,0xAA,0xBF,0xBF,0xBB,0xAE,0xBF,0xBB,0xAA,0xAB,0x6A,0xFA,0xFB,0xFF,
-	0xEF,0xFE,0xBE,0xBE,0xFA,0xFB,0xEA,0xAF,0xFF,0xBF,0xFE,0xAA,0xBA,0xB6,0xAF,0xAF,0xAF,0xFA,0xAF,0xAB,
-	0xFF,0xEA,0xFA,0xCC,0x07,0xAA,0xAB,0x6A,0xCC,0x05,0xAA,0xAB,0x6A,0xCC,0x0D,0xAA,0xAB,0x6A,0xAA,0xAA,
-	0xAA,0xAB,0x6A,0xAA,0xAA,0xAA,0xAB,0x6A,0xCC,0x07,0xAA,0xAB,0x6A,0xCC,0x0F,0xAA,0xB6,0xCC,0x10,0xAA,
-	0xAB,0xBF,0xCC,0x06,0xFF,0xBF,0xCC,0x0E,0xFF,0xBF,0xCC,0x03,0xFF,0xBF,0xCC,0x03,0xFF,0xBF,0xCC,0x08,
-	0xFF,0xBF,0xCC,0x0F,0xFF,0xFB,0xCC,0x11,0xFF,0xCC,0x06,0x55,0x56,0xCC,0x06,0x55,0x56,0xCC,0x06,0x55,
+	0x56,0xCC,0x03,0x55,0x56,0xCC,0x03,0x55,0x56,0xCC,0x08,0x55,0x56,0xCC,0x07,0x55,0x59,0xCC,0x07,0x55,
+	0x65,0xCC,0x10,0x55,0x56,0x65,0x57,0xE9,0x7A,0x95,0x7D,0x7A,0xAB,0x6A,0xCC,0x0D,0xAA,0xAB,0x6A,0xAA,
+	0xAA,0xAA,0xAB,0x6A,0xAA,0xAA,0xAA,0xAB,0x6A,0xCC,0x07,0xAA,0xAB,0x6A,0xCC,0x06,0xAA,0xAD,0xCC,0x07,
+	0xAA,0xB6,0xCC,0x10,0xAA,0xAB,0x6B,0xFF,0xAA,0xFA,0xAF,0xFA,0xFA,0xAB,0x6A,0xCC,0x0D,0xAA,0xAB,0x6A,
+	0xAA,0xAA,0xAA,0xAB,0x6A,0xAA,0xAA,0xAA,0xAB,0x6A,0xCC,0x07,0xAA,0xAB,0x6A,0xCC,0x06,0xAA,0xAD,0xCC,
+	0x07,0xAA,0xB6,0xCC,0x10,0xAA,0xAB,0x6A,0xCC,0x05,0xAA,0xAB,0x6A,0xAA,0xAA,0xA9,0x55,0xA9,0x56,0x96,
+	0x96,0x95,0x59,0x55,0x6A,0xAA,0xAA,0xAB,0x6A,0x5A,0x56,0x56,0xAB,0x6A,0x56,0x5A,0x5A,0xAB,0x6A,0xAA,
+	0x56,0x56,0x5A,0x56,0x56,0x56,0xAA,0xAB,0x6A,0xA5,0x65,0x66,0x65,0x66,0xA5,0x6A,0xAD,0x95,0x95,0x95,
+	0x99,0x95,0x9A,0xAA,0x6A,0xB6,0x96,0x96,0x95,0x69,0x55,0x65,0x55,0xA9,0x6A,0xCC,0x07,0xAA,0xAB,0x6A,
+	0xCC,0x05,0xAA,0xAB,0x6A,0xAA,0xAA,0xA9,0x7D,0x65,0xF5,0x95,0x97,0x5F,0xFD,0x7F,0xFA,0xAA,0xAA,0xAB,
+	0x6A,0x76,0x7F,0x7F,0xAB,0x6A,0x7F,0x76,0x76,0xAB,0x6A,0xAA,0x7F,0x7F,0x76,0x9F,0x7F,0x77,0xAA,0xAB,
+	0x6A,0xA7,0xF7,0x75,0x77,0x77,0xA7,0xFA,0xAD,0x9D,0xDF,0xDF,0xD5,0xDD,0xDE,0xAA,0x5A,0xB6,0x95,0x97,
+	0x5F,0x5A,0xD7,0xF5,0xFF,0xE9,0x7A,0xCC,0x07,0xAA,0xAB,0x6A,0xCC,0x05,0xAA,0xAB,0x6A,0xAA,0xAA,0xA9,
+	0x55,0xF5,0x55,0xD5,0x57,0x5D,0x59,0x56,0xAA,0xAA,0xAA,0xAB,0x6A,0x5B,0x5A,0x76,0xAB,0x6A,0x5A,0x77,
+	0x77,0xAB,0x6A,0xAA,0x7A,0x5A,0x77,0x9E,0x5A,0x5B,0xAA,0xAB,0x6A,0xA5,0x65,0x77,0x75,0x77,0xA5,0xAA,
+	0xAD,0x96,0xD6,0x95,0x9D,0xD5,0xDE,0xAA,0x56,0xB6,0x95,0x57,0x5E,0x5E,0x97,0xA5,0x5A,0xAA,0xFA,0xCC,
+	0x07,0xAA,0xAB,0x6A,0xCC,0x05,0xAA,0xAB,0x6A,0xAA,0xAA,0xA9,0x75,0xE5,0xF5,0xD7,0x57,0x5E,0x5D,0x7F,
+	0xAA,0xAA,0xAA,0xAB,0x6A,0x76,0x7E,0x77,0xAB,0x6A,0x7E,0x77,0x77,0xAB,0x6A,0xAA,0x7A,0x7E,0x77,0x9E,
+	0x7E,0x76,0xAA,0xAB,0x6A,0xAB,0x77,0x77,0x77,0xF7,0xA7,0xEA,0xAD,0x9D,0x9F,0xAD,0xDD,0xDF,0xDE,0xAA,
+	0x5F,0xB6,0x97,0x57,0x5E,0x5E,0x97,0xA5,0xFE,0xA9,0x6A,0xCC,0x07,0xAA,0xAB,0x6A,0xCC,0x05,0xAA,0xAB,
+	0x6A,0xAA,0xAA,0xA9,0x79,0x65,0xE5,0xD7,0x97,0x95,0x5D,0x55,0x6A,0xAA,0xAA,0xAB,0x6A,0x5B,0x56,0x57,
+	0xAB,0x6A,0x56,0x77,0x5B,0xAB,0x6A,0xAA,0x56,0x56,0x77,0x9E,0x56,0x77,0xAA,0xAB,0x6A,0xA5,0x77,0x77,
+	0x77,0xA5,0x65,0x6A,0xAD,0x9D,0xD5,0x95,0xDD,0xDE,0x95,0x9A,0x7E,0xB6,0x97,0x97,0x95,0x7E,0x97,0xA5,
+	0x55,0xA9,0x7A,0xCC,0x07,0xAA,0xAB,0x6A,0xCC,0x05,0xAA,0xAB,0x6A,0xAA,0xAA,0xAA,0xFA,0xFB,0xEB,0xEF,
+	0xAF,0xAF,0xFE,0xFF,0xFA,0xAA,0xAA,0xAB,0x6A,0xBE,0xBF,0xBF,0xAB,0x6A,0xBF,0xBB,0xBE,0xAB,0x6A,0xAA,
+	0xBF,0xBF,0xBB,0xAE,0xBF,0xBB,0xAA,0xAB,0x6A,0xAB,0xFB,0xBB,0xBB,0xAB,0xFB,0xFA,0xAD,0xAE,0xEF,0xEF,
+	0xEE,0xEE,0xAF,0xEE,0xBA,0xB6,0xAF,0xAF,0xAF,0xFA,0xAF,0xAB,0xFF,0xEA,0xFA,0xCC,0x07,0xAA,0xAB,0x6A,
+	0xCC,0x05,0xAA,0xAB,0x6A,0xCC,0x0D,0xAA,0xAB,0x6A,0xAA,0xAA,0xAA,0xAB,0x6A,0xAA,0xAA,0xAA,0xAB,0x6A,
+	0xCC,0x07,0xAA,0xAB,0x6A,0xCC,0x06,0xAA,0xAD,0xCC,0x07,0xAA,0xB6,0xCC,0x10,0xAA,0xAB,0xBF,0xCC,0x06,
+	0xFF,0xBF,0xCC,0x0E,0xFF,0xBF,0xCC,0x03,0xFF,0xBF,0xCC,0x03,0xFF,0xBF,0xCC,0x08,0xFF,0xBF,0xCC,0x06,
+	0xFF,0xFE,0xCC,0x07,0xFF,0xFB,0xCC,0x11,0xFF,0xCC,0x06,0x55,0x56,0xCC,0x06,0x55,0x56,0xCC,0x06,0x55,
 	0x56,0xCC,0x08,0x55,0x56,0xCC,0x08,0x55,0x56,0xCC,0x07,0x55,0x59,0xCC,0x07,0x55,0x65,0xCC,0x10,0x55,
 	0x56,0x6A,0xCC,0x05,0xAA,0xAB,0x6A,0xCC,0x05,0xAA,0xAB,0x6A,0xCC,0x05,0xAA,0xAB,0x6A,0xCC,0x07,0xAA,
 	0xAB,0x6A,0xCC,0x07,0xAA,0xAB,0x6A,0xCC,0x06,0xAA,0xAD,0xCC,0x07,0xAA,0xB6,0xCC,0x10,0xAA,0xAB,0x6A,
--- a/src/gfx/pt2_gfx_spectrum.c
+++ b/src/gfx/pt2_gfx_spectrum.c
@@ -154,4 +154,4 @@
 	0x6A,0xCC,0x2F,0xAA,0xAB,0xBF,0xCC,0x30,0xFF
 };
 
-uint32_t spectrumAnaBMP[36];
+uint32_t analyzerColorsRGB24[36];
--- a/src/pt2_audio.c
+++ b/src/pt2_audio.c
@@ -31,6 +31,8 @@
 #include "pt2_visuals.h"
 #include "pt2_scopes.h"
 #include "pt2_mod2wav.h"
+#include "pt2_pat2smp.h"
+#include "pt2_sync.h"
 #include "pt2_structs.h"
 
 #define INITIAL_DITHER_SEED 0x12345000
@@ -41,37 +43,60 @@
 	double c, ci, feedback, bg, cg, c2;
 } ledFilter_t;
 
-typedef struct voice_t
-{
-	volatile bool active;
-	const int8_t *data, *newData;
-	int32_t length, newLength, pos;
-	double dVolume, dDelta, dDeltaMul, dPhase, dLastDelta, dLastDeltaMul, dLastPhase, dPanL, dPanR;
-} paulaVoice_t;
-
-audio_t audio; // globalized
-
 static volatile int8_t filterFlags;
 static int8_t defStereoSep;
 static bool amigaPanFlag;
 static uint16_t ch1Pan, ch2Pan, ch3Pan, ch4Pan;
-static int32_t oldPeriod, randSeed = INITIAL_DITHER_SEED;
-static uint32_t oldScopeDelta, sampleCounter;
+static int32_t oldPeriod = -1, randSeed = INITIAL_DITHER_SEED;
+static uint32_t sampleCounter, audLatencyPerfValInt, audLatencyPerfValFrac;
+static uint64_t tickTime64, tickTime64Frac;
 static double *dMixBufferL, *dMixBufferR, *dMixBufferLUnaligned, *dMixBufferRUnaligned, dOldVoiceDelta, dOldVoiceDeltaMul;
 static double dPrngStateL, dPrngStateR;
 static blep_t blep[AMIGA_VOICES], blepVol[AMIGA_VOICES];
-static rcFilter_t filterLo, filterHi;
+static rcFilter_t filterLoA500, filterLoA1200, filterHi;
 static ledFilter_t filterLED;
-static paulaVoice_t paula[AMIGA_VOICES];
 static SDL_AudioDeviceID dev;
 
+// for audio/video syncing
+static uint32_t tickTimeLen, tickTimeLenFrac;
+
 // globalized
+audio_t audio;
 uint32_t samplesPerTick;
+paulaVoice_t paula[AMIGA_VOICES];
 
 bool intMusic(void); // defined in pt_modplayer.c
 
-static uint16_t bpm2SmpsPerTick(int32_t bpm, uint32_t audioFreq)
+static void calcAudioLatencyVars(int32_t audioBufferSize, int32_t audioFreq)
 {
+	double dInt, dFrac;
+
+	if (audioFreq == 0)
+		return;
+
+	const double dAudioLatencySecs = audioBufferSize / (double)audioFreq;
+
+	dFrac = modf(dAudioLatencySecs * editor.dPerfFreq, &dInt);
+
+	// integer part
+	audLatencyPerfValInt = (int32_t)dInt;
+
+	// fractional part (scaled to 0..2^32-1)
+	dFrac *= UINT32_MAX;
+	dFrac += 0.5;
+	if (dFrac > UINT32_MAX)
+		dFrac = UINT32_MAX;
+	audLatencyPerfValFrac = (uint32_t)dFrac;
+}
+
+void setSyncTickTimeLen(uint32_t timeLen, uint32_t timeLenFrac)
+{
+	tickTimeLen = timeLen;
+	tickTimeLenFrac = timeLenFrac;
+}
+
+static uint16_t bpm2SmpsPerTick(int32_t bpm, double dAudioFreq)
+{
 	if (bpm == 0)
 		return 0;
 
@@ -78,7 +103,7 @@
 	const int32_t ciaVal = (int32_t)(1773447 / bpm); // yes, PT truncates here
 	const double dCiaHz = (double)CIA_PAL_CLK / ciaVal;
 
-	int32_t smpsPerTick = (int32_t)((audioFreq / dCiaHz) + 0.5); // rounded
+	int32_t smpsPerTick = (int32_t)((dAudioFreq / dCiaHz) + 0.5); // rounded
 	return (uint16_t)smpsPerTick;
 }
 
@@ -87,8 +112,8 @@
 	for (int32_t i = 32; i <= 255; i++)
 	{
 		audio.bpmTab[i-32] = bpm2SmpsPerTick(i, audio.outputRate);
-		audio.bpmTab28kHz[i-32] = bpm2SmpsPerTick(i, 28836); // PAT2SMP hi quality
-		audio.bpmTab22kHz[i-32] = bpm2SmpsPerTick(i, 22168); // PAT2SMP low quality
+		audio.bpmTab28kHz[i-32] = bpm2SmpsPerTick(i, PAT2SMP_HI_FREQ); // PAT2SMP hi quality
+		audio.bpmTab22kHz[i-32] = bpm2SmpsPerTick(i, PAT2SMP_LO_FREQ); // PAT2SMP low quality
 		audio.bpmTabMod2Wav[i-32] = bpm2SmpsPerTick(i, MOD2WAV_FREQ); // MOD2WAV
 	}
 }
@@ -101,10 +126,10 @@
 	filterLED.buffer[3] = 0.0;
 }
 
-void setLEDFilter(bool state)
+void setLEDFilter(bool state, bool doLockAudio)
 {
 	const bool audioWasntLocked = !audio.locked;
-	if (audioWasntLocked)
+	if (doLockAudio && audioWasntLocked)
 		lockAudio();
 
 	editor.useLEDFilter = state;
@@ -118,7 +143,7 @@
 		filterFlags &= ~FILTER_LED_ENABLED;
 	}
 
-	if (audioWasntLocked)
+	if (doLockAudio && audioWasntLocked)
 		unlockAudio();
 }
 
@@ -215,7 +240,8 @@
 
 void calcRCFilterCoeffs(double dSr, double dHz, rcFilter_t *f)
 {
-	f->c = tan((M_PI * dHz) / dSr);
+	const double c = (dHz < (dSr / 2.0)) ? tan((M_PI * dHz) / dSr) : 1.0;
+	f->c = c;
 	f->c2 = f->c * 2.0;
 	f->g = 1.0 / (1.0 + f->c);
 	f->cg = f->c * f->g;
@@ -278,24 +304,6 @@
 	*out = in - low; // high-pass
 }
 
-/* aciddose: these sin/cos approximations both use a 0..1
-** parameter range and have 'normalized' (1/2 = 0db) coeffs
-**
-** the coeffs are for LERP(x, x * x, 0.224) * sqrt(2)
-** max_error is minimized with 0.224 = 0.0013012886
-*/
-static double sinApx(double fX)
-{
-	fX = fX * (2.0 - fX);
-	return fX * 1.09742972 + fX * fX * 0.31678383;
-}
-
-static double cosApx(double fX)
-{
-	fX = (1.0 - fX) * (1.0 + fX);
-	return fX * 1.09742972 + fX * fX * 0.31678383;
-}
-
 void lockAudio(void)
 {
 	if (dev != 0)
@@ -302,6 +310,9 @@
 		SDL_LockAudioDevice(dev);
 
 	audio.locked = true;
+
+	audio.resetSyncTickTimeFlag = true;
+	resetChSyncQueue();
 }
 
 void unlockAudio(void)
@@ -309,6 +320,9 @@
 	if (dev != 0)
 		SDL_UnlockAudioDevice(dev);
 
+	audio.resetSyncTickTimeFlag = true;
+	resetChSyncQueue();
+
 	audio.locked = false;
 }
 
@@ -323,19 +337,41 @@
 
 			paulaSetData(i, ch->n_start + s->loopStart);
 			paulaSetLength(i, s->loopLength >> 1);
+
+			if (!editor.songPlaying)
+			{
+				scopeSetData(i, ch->n_start + s->loopStart);
+				scopeSetLength(i, s->loopLength >> 1);
+			}
 		}
 	}
 }
 
-static void mixerSetVoicePan(uint8_t ch, uint16_t pan) // pan = 0..256
+/* aciddose: these sin/cos approximations both use a 0..1
+** parameter range and have 'normalized' (1/2 = 0db) coeffs
+**
+** the coeffs are for LERP(x, x * x, 0.224) * sqrt(2)
+** max_error is minimized with 0.224 = 0.0013012886
+*/
+static double sinApx(double x)
 {
-	double dPan;
+	x = x * (2.0 - x);
+	return x * 1.09742972 + x * x * 0.31678383;
+}
 
+static double cosApx(double x)
+{
+	x = (1.0 - x) * (1.0 + x);
+	return x * 1.09742972 + x * x * 0.31678383;
+}
+
+static void mixerSetVoicePan(uint8_t ch, uint16_t pan) // pan = 0..256
+{
 	/* aciddose: proper 'normalized' equal-power panning is (assuming pan left to right):
 	** L = cos(p * pi * 1/2) * sqrt(2);
 	** R = sin(p * pi * 1/2) * sqrt(2);
 	*/
-	dPan = pan * (1.0 / 256.0); // 0.0..1.0
+	const double dPan = pan * (1.0 / 256.0); // 0.0..1.0
 
 	paula[ch].dPanL = cosApx(dPan);
 	paula[ch].dPanR = sinApx(dPan);
@@ -374,7 +410,8 @@
 	for (int32_t i = 0; i < AMIGA_VOICES; i++)
 		mixerKillVoice(i);
 
-	clearRCFilterState(&filterLo);
+	clearRCFilterState(&filterLoA500);
+	clearRCFilterState(&filterLoA1200);
 	clearRCFilterState(&filterHi);
 	clearLEDFilterState();
 
@@ -401,6 +438,9 @@
 
 	v = &paula[ch];
 
+	v->syncPeriod = period; // used for pt2_sync.c
+	v->syncFlags |= UPDATE_PERIOD; // used for pt2_sync.c
+
 	if (period == 0)
 		realPeriod = 1+65535; // confirmed behavior on real Amiga
 	else if (period < 113)
@@ -413,26 +453,22 @@
 	{
 		oldPeriod = realPeriod;
 
-		// this period is not cached, calculate mixer/scope deltas
+		// this period is not cached, calculate mixer deltas
 
 		// during PAT2SMP or doing MOD2WAV, use different audio output rates
 		if (editor.isSMPRendering)
-			dPeriodToDeltaDiv = editor.pat2SmpHQ ? (PAULA_PAL_CLK / 28836.0) : (PAULA_PAL_CLK / 22168.0);
+			dPeriodToDeltaDiv = editor.pat2SmpHQ ? (PAULA_PAL_CLK / PAT2SMP_HI_FREQ) : (PAULA_PAL_CLK / PAT2SMP_LO_FREQ);
 		else if (editor.isWAVRendering)
 			dPeriodToDeltaDiv = PAULA_PAL_CLK / (double)MOD2WAV_FREQ;
 		else
 			dPeriodToDeltaDiv = audio.dPeriodToDeltaDiv;
 
-		const double dPeriodToScopeDeltaDiv = ((double)PAULA_PAL_CLK * SCOPE_FRAC_SCALE) / SCOPE_HZ;
-
 		// cache these
 		dOldVoiceDelta = dPeriodToDeltaDiv / realPeriod;
-		oldScopeDelta = (int32_t)((dPeriodToScopeDeltaDiv / realPeriod) + 0.5);
 		dOldVoiceDeltaMul = 1.0 / dOldVoiceDelta; // for BLEP synthesis
 	}
 
 	v->dDelta = dOldVoiceDelta;
-	scope[ch].delta = oldScopeDelta;
 
 	// for BLEP synthesis
 	v->dDeltaMul = dOldVoiceDeltaMul;
@@ -442,13 +478,19 @@
 
 void paulaSetVolume(int32_t ch, uint16_t vol)
 {
+	paulaVoice_t *v;
+
+	v = &paula[ch];
+
 	vol &= 127; // confirmed behavior on real Amiga
 
 	if (vol > 64)
 		vol = 64; // confirmed behavior on real Amiga
 
-	paula[ch].dVolume = vol * (1.0 / 64.0);
-	scope[ch].volume = (uint8_t)vol;
+	v->dVolume = vol * (1.0 / 64.0);
+
+	v->syncVolume = (int8_t)vol; // used for pt2_sync.c
+	v->syncFlags |= UPDATE_VOLUME; // used for pt2_sync.c
 }
 
 void paulaSetLength(int32_t ch, uint16_t len)
@@ -461,7 +503,8 @@
 		*/
 	}
 
-	scope[ch].newLength = paula[ch].newLength = len << 1; // our mixer works with bytes, not words
+	paula[ch].newLength = len << 1; // our mixer works with bytes, not words
+	paula[ch].syncFlags |= UPDATE_LENGTH; // for pt2_sync.c
 }
 
 void paulaSetData(int32_t ch, const int8_t *src)
@@ -470,12 +513,13 @@
 	if (src == NULL)
 		src = &song->sampleData[RESERVED_SAMPLE_OFFSET]; // dummy sample
 
-	scope[ch].newData = paula[ch].newData = src;
+	paula[ch].newData = src;
+	paula[ch].syncFlags |= UPDATE_DATA; // for pt2_sync.c
 }
 
 void paulaStopDMA(int32_t ch)
 {
-	scope[ch].active = paula[ch].active = false;
+	paula[ch].active = false;
 }
 
 void paulaStartDMA(int32_t ch)
@@ -502,7 +546,10 @@
 	v->length = length;
 	v->active = true;
 
-	scopeTrigger(ch, length);
+	// for pt2_sync.c
+	v->syncTriggerData = dat;
+	v->syncTriggerLength = (uint16_t)(length >> 1);
+	v->syncFlags |= TRIGGER_SAMPLE;
 }
 
 void toggleA500Filters(void)
@@ -511,7 +558,8 @@
 	if (audioWasntLocked)
 		lockAudio();
 
-	clearRCFilterState(&filterLo);
+	clearRCFilterState(&filterLoA500);
+	clearRCFilterState(&filterLoA1200);
 	clearRCFilterState(&filterHi);
 	clearLEDFilterState();
 
@@ -539,15 +587,15 @@
 	memset(dMixBufferL, 0, numSamples * sizeof (double));
 	memset(dMixBufferR, 0, numSamples * sizeof (double));
 
-	for (int32_t i = 0; i < AMIGA_VOICES; i++)
+	v = paula;
+	bSmp = blep;
+	bVol = blepVol;
+
+	for (int32_t i = 0; i < AMIGA_VOICES; i++, v++, bSmp++, bVol++)
 	{
-		v = &paula[i];
 		if (!v->active || v->data == NULL)
 			continue;
 
-		bSmp = &blep[i];
-		bVol = &blepVol[i];
-
 		for (int32_t j = 0; j < numSamples; j++)
 		{
 			assert(v->data != NULL);
@@ -580,7 +628,7 @@
 			dMixBufferR[j] += dSmp * v->dPanR;
 
 			v->dPhase += v->dDelta;
-			if (v->dPhase >= 1.0)
+			if (v->dPhase >= 1.0) // deltas can't be >= 1.0, so this is safe
 			{
 				v->dPhase -= 1.0;
 
@@ -610,15 +658,15 @@
 	memset(dMixBufferL, 0, numSamples * sizeof (double));
 	memset(dMixBufferR, 0, numSamples * sizeof (double));
 
-	for (int32_t i = 0; i < AMIGA_VOICES; i++)
+	v = paula;
+	bSmp = blep;
+	bVol = blepVol;
+
+	for (int32_t i = 0; i < AMIGA_VOICES; i++, v++, bSmp++, bVol++)
 	{
-		v = &paula[i];
 		if (!v->active || v->data == NULL)
 			continue;
 
-		bSmp = &blep[i];
-		bVol = &blepVol[i];
-
 		for (int32_t j = 0; j < numSamples; j++)
 		{
 			assert(v->data != NULL);
@@ -651,7 +699,7 @@
 			dMixBufferR[j] += dSmp * v->dPanR;
 
 			v->dPhase += v->dDelta;
-			while (v->dPhase >= 1.0) // multi-step
+			while (v->dPhase >= 1.0) // deltas can be >= 1.0 here
 			{
 				v->dPhase -= 1.0;
 
@@ -695,7 +743,11 @@
 	dOut[0] = dMixBufferL[i];
 	dOut[1] = dMixBufferR[i];
 
-	// don't process any low-pass filter since the cut-off is around 34kHz on A1200
+	if (audio.outputRate >= 96000) // cutoff is too high for 44.1kHz/48kHz
+	{
+		// process low-pass filter
+		RCLowPassFilter(&filterLoA1200, dOut, dOut);
+	}
 
 	// process high-pass filter
 	RCHighPassFilter(&filterHi, dOut, dOut);
@@ -729,7 +781,11 @@
 	dOut[0] = dMixBufferL[i];
 	dOut[1] = dMixBufferR[i];
 
-	// don't process any low-pass filter since the cut-off is around 34kHz on A1200
+	if (audio.outputRate >= 96000) // cutoff is too high for 44.1kHz/48kHz
+	{
+		// process low-pass filter
+		RCLowPassFilter(&filterLoA1200, dOut, dOut);
+	}
 
 	// process "LED" filter
 	LEDFilter(&filterLED, dOut, dOut);
@@ -767,7 +823,7 @@
 	dOut[1] = dMixBufferR[i];
 
 	// process low-pass filter
-	RCLowPassFilter(&filterLo, dOut, dOut);
+	RCLowPassFilter(&filterLoA500, dOut, dOut);
 
 	// process high-pass filter
 	RCHighPassFilter(&filterHi, dOut, dOut);
@@ -801,7 +857,7 @@
 	dOut[1] = dMixBufferR[i];
 
 	// process low-pass filter
-	RCLowPassFilter(&filterLo, dOut, dOut);
+	RCLowPassFilter(&filterLoA500, dOut, dOut);
 
 	// process "LED" filter
 	LEDFilter(&filterLED, dOut, dOut);
@@ -829,8 +885,7 @@
 	out[1] = (int16_t)smp32;
 }
 
-// for PAT2SMP
-static inline void processMixedSamplesRaw(int32_t i, int16_t *out)
+static inline void processMixedSamplesRaw(int32_t i, int16_t *out) // for PAT2SMP
 {
 	int32_t smp32;
 	double dOut[2];
@@ -838,7 +893,7 @@
 	dOut[0] = dMixBufferL[i];
 	dOut[1] = dMixBufferR[i];
 
-	// normalize 
+	// normalize (don't flip the phase this time)
 	dOut[0] *= (INT16_MAX / (double)AMIGA_VOICES);
 	dOut[1] *= (INT16_MAX / (double)AMIGA_VOICES);
 
@@ -846,7 +901,7 @@
 
 	smp32 = (int32_t)dOut[0];
 	CLAMP16(smp32);
-	out[0] = (int16_t)smp32;
+	*out = (int16_t)smp32;
 }
 
 void outputAudio(int16_t *target, int32_t numSamples)
@@ -934,6 +989,50 @@
 	}
 }
 
+static void fillVisualsSyncBuffer(void)
+{
+	chSyncData_t chSyncData;
+
+	if (audio.resetSyncTickTimeFlag)
+	{
+		audio.resetSyncTickTimeFlag = false;
+
+		tickTime64 = SDL_GetPerformanceCounter() + audLatencyPerfValInt;
+		tickTime64Frac = audLatencyPerfValFrac;
+	}
+
+	moduleChannel_t *c = song->channels;
+	paulaVoice_t *v = paula;
+	syncedChannel_t *s = chSyncData.channels;
+
+	for (int32_t i = 0; i < AMIGA_VOICES; i++, c++, s++, v++)
+	{
+		s->flags = v->syncFlags | c->syncFlags;
+		c->syncFlags = v->syncFlags = 0; // clear sync flags
+
+		s->volume = v->syncVolume;
+		s->period = v->syncPeriod;
+		s->triggerData = v->syncTriggerData;
+		s->triggerLength = v->syncTriggerLength;
+		s->newData = v->newData;
+		s->newLength = (uint16_t)(v->newLength >> 1);
+		s->vuVolume = c->syncVuVolume;
+		s->analyzerVolume = c->syncAnalyzerVolume;
+		s->analyzerPeriod = c->syncAnalyzerPeriod;
+	}
+
+	chSyncData.timestamp = tickTime64;
+	chQueuePush(chSyncData);
+
+	tickTime64 += tickTimeLen;
+	tickTime64Frac += tickTimeLenFrac;
+	if (tickTime64Frac > 0xFFFFFFFF)
+	{
+		tickTime64Frac &= 0xFFFFFFFF;
+		tickTime64++;
+	}
+}
+
 static void SDLCALL audioCallback(void *userdata, Uint8 *stream, int len)
 {
 	int16_t *streamOut;
@@ -952,8 +1051,13 @@
 	{
 		if (sampleCounter == 0)
 		{
+			// new replayer tick
+
 			if (editor.songPlaying)
+			{
 				intMusic();
+				fillVisualsSyncBuffer();
+			}
 
 			sampleCounter = samplesPerTick;
 		}
@@ -986,7 +1090,7 @@
 	** Low Hz = 4420.97~ = 1 / (2pi * 360 * 0.0000001)
 	**
 	** Under spice simulation the circuit yields -3dB = 4400Hz.
-	** In the Amiga 1200, the low-pass cutoff is 26kHz+, so the
+	** In the Amiga 1200, the low-pass cutoff is ~34kHz, so the
 	** static low-pass filter is disabled in the mixer in A1200 mode.
 	**
 	** Next comes a bog-standard Sallen-Key filter ("LED") with:
@@ -1008,35 +1112,65 @@
 	**
 	** High Hz = 5.2~ = 1 / (2pi * 1390 * 0.000022)
 	** Under spice simulation the circuit yields -3dB = 5.2Hz.
+	**
+	** 8bitbubsy:
+	** Keep in mind that many of the Amiga schematics that are floating around on
+	** the internet have wrong RC values! They were most likely very early schematics
+	** that didn't change before production (or changes that never reached production).
+	** This has been confirmed by measuring the components on several Amiga motherboards.
+	**
+	** Correct values for A500 (A500_R6.pdf):
+	** - RC 6dB/oct low-pass: R=360 ohm, C=0.1uF (f=4420.970Hz)
+	** - Sallen-key low-pass ("LED"): R1/R2=10k ohm, C1=6800pF, C2=3900pF (f=3090.532Hz)
+	** - RC 6dB/oct high-pass: R=1390 ohm (1000+390), C=22.33uF (22+0.33) (f=5.127Hz)
+	**
+	** Correct values for A1200 (A1200_R2.pdf):
+	** - RC 6dB/oct low-pass: R=680 ohm, C=6800pF (f=34419.321Hz)
+	** - Sallen-key low-pass ("LED"): Same as A500 (f=3090.532Hz)
+	** - RC 6dB/oct high-pass: R=1390 ohm (1000+390), C=22uF (f=5.204Hz)
+	**
+	** Correct values for A600 (a600_schematics.pdf):
+	** - RC 6dB/oct low-pass: Same as A500 (f=4420.970Hz)
+	** - Sallen-key low-pass ("LED"): Same as A500 (f=3090.532Hz)
+	** - RC 6dB/oct high-pass: Same as A1200 (f=5.204Hz)
 	*/
 
 	double R, C, R1, R2, C1, C2, fc, fb;
 
+	if (audio.outputRate >= 96000) // cutoff is too high for 44.1kHz/48kHz
+	{
+		// A1200 one-pole 6db/oct static RC low-pass filter:
+		R = 680.0;  // R321 (680 ohm resistor)
+		C = 6.8e-9; // C321 (6800pf capacitor)
+		fc = 1.0 / (2.0 * M_PI * R * C);
+		calcRCFilterCoeffs(audio.outputRate, fc, &filterLoA1200);
+	}
+
 	// A500 one-pole 6db/oct static RC low-pass filter:
 	R = 360.0; // R321 (360 ohm resistor)
 	C = 1e-7;  // C321 (0.1uF capacitor)
-	fc = 1.0 / (2.0 * M_PI * R * C); // ~4420.97Hz
-	calcRCFilterCoeffs(audio.outputRate, fc, &filterLo);
+	fc = 1.0 / (2.0 * M_PI * R * C);
+	calcRCFilterCoeffs(audio.outputRate, fc, &filterLoA500);
 
-	// A500/A1200 Sallen-Key filter ("LED"):
+	// Sallen-Key filter ("LED"):
 	R1 = 10000.0; // R322 (10K ohm resistor)
 	R2 = 10000.0; // R323 (10K ohm resistor)
 	C1 = 6.8e-9;  // C322 (6800pF capacitor)
 	C2 = 3.9e-9;  // C323 (3900pF capacitor)
-	fc = 1.0 / (2.0 * M_PI * sqrt(R1 * R2 * C1 * C2)); // ~3090.53Hz
-	fb = 0.125; // Fb = 0.125 : Q ~= 1/sqrt(2) (Butterworth)
+	fc = 1.0 / (2.0 * M_PI * sqrt(R1 * R2 * C1 * C2));
+	fb = 0.125; // Fb = 0.125 : Q ~= 1/sqrt(2)
 	calcLEDFilterCoeffs(audio.outputRate, fc, fb, &filterLED);
 
-	// A500/A1200 one-pole 6db/oct static RC high-pass filter:
-	R = 1000.0 + 390.0; // R324 (1K ohm resistor) + R325 (390 ohm resistor)
-	C = 2.2e-5;         // C334 (22uF capacitor) (+ C324 (0.33uF capacitor) if A500)
-	fc = 1.0 / (2.0 * M_PI * R * C); // ~5.2Hz
+	// A1200 one-pole 6db/oct static RC high-pass filter:
+	R = 1390.0; // R324 (1K ohm resistor) + R325 (390 ohm resistor)
+	C = 2.2e-5; // C334 (22uF capacitor)
+	fc = 1.0 / (2.0 * M_PI * R * C);
 	calcRCFilterCoeffs(audio.outputRate, fc, &filterHi);
 }
 
 void mixerCalcVoicePans(uint8_t stereoSeparation)
 {
-	uint8_t scaledPanPos = (stereoSeparation * 128) / 100;
+	const uint8_t scaledPanPos = (stereoSeparation * 128) / 100;
 
 	ch1Pan = 128 - scaledPanPos;
 	ch2Pan = 128 + scaledPanPos;
@@ -1117,6 +1251,15 @@
 	samplesPerTick = audio.bpmTab[125-32]; // BPM 125
 	sampleCounter = 0;
 
+	calcAudioLatencyVars(audio.audioBufferSize, audio.outputRate);
+	for (int32_t i = 0; i < 256-32; i++)
+	{
+		// number of samples per tick -> tick length for performance counter (syncing visuals to audio)
+		const double dTickTimeLenMul = (editor.dPerfFreq / audio.outputRate) * (UINT32_MAX + 1.0);
+		audio.tickTimeLengthTab[i] = (uint64_t)(audio.bpmTab[i] * dTickTimeLenMul);
+	}
+
+	audio.resetSyncTickTimeFlag = true;
 	SDL_PauseAudioDevice(dev, false);
 	return true;
 }
--- a/src/pt2_audio.h
+++ b/src/pt2_audio.h
@@ -2,6 +2,7 @@
 
 #include <stdint.h>
 #include <stdbool.h>
+#include "pt2_header.h" // AMIGA_VOICES
 
 // adding this forces the FPU to enter slow mode
 #define DENORMAL_OFFSET 1e-10
@@ -14,15 +15,38 @@
 
 typedef struct audio_t
 {
-	volatile bool locked;
+	volatile bool locked, isSampling;
+
 	bool forceMixerOff;
 	uint16_t bpmTab[256-32], bpmTab28kHz[256-32], bpmTab22kHz[256-32], bpmTabMod2Wav[256-32];
 	uint32_t outputRate, audioBufferSize;
 	double dPeriodToDeltaDiv;
+
+	// for audio sampling
+	bool rescanAudioDevicesSupported;
+
+	// for audio/video syncing
+	bool resetSyncTickTimeFlag;
+	uint64_t tickTimeLengthTab[224];
 } audio_t;
 
-extern audio_t audio; // pt2_audio.c
+typedef struct voice_t
+{
+	volatile bool active;
 
+	const int8_t *data, *newData;
+	int32_t length, newLength, pos;
+	double dVolume, dDelta, dDeltaMul, dPhase, dLastDelta, dLastDeltaMul, dLastPhase, dPanL, dPanR;
+
+	// used for pt2_sync.c
+	uint8_t syncFlags;
+	int8_t syncVolume;
+	uint16_t syncPeriod;
+	uint16_t syncTriggerLength;
+	const int8_t *syncTriggerData;
+} paulaVoice_t;
+
+void setSyncTickTimeLen(uint32_t timeLen, uint32_t timeLenFrac);
 void resetCachedMixerPeriod(void);
 void resetAudioDithering(void);
 void calcRCFilterCoeffs(const double sr, const double hz, rcFilter_t *f);
@@ -35,7 +59,7 @@
 void normalize16bitSigned(int16_t *sampleData, uint32_t sampleLength);
 void normalize8bitFloatSigned(float *fSampleData, uint32_t sampleLength);
 void normalize8bitDoubleSigned(double *dSampleData, uint32_t sampleLength);
-void setLEDFilter(bool state);
+void setLEDFilter(bool state, bool doLockAudio);
 void toggleLEDFilter(void);
 void toggleAmigaPanMode(void);
 void toggleA500Filters(void);
@@ -54,3 +78,6 @@
 void mixerSetSamplesPerTick(uint32_t val);
 void mixerClearSampleCounter(void);
 void outputAudio(int16_t *target, int32_t numSamples);
+
+extern audio_t audio; // pt2_audio.c
+extern paulaVoice_t paula[AMIGA_VOICES]; // pt2_audio.c
--- a/src/pt2_bmp.c
+++ b/src/pt2_bmp.c
@@ -17,7 +17,8 @@
 uint32_t *editOpScreen3BMP = NULL, *editOpScreen4BMP   = NULL, *spectrumVisualsBMP = NULL;
 uint32_t *muteButtonsBMP   = NULL, *posEdBMP           = NULL, *samplerFiltersBMP  = NULL;
 uint32_t *samplerScreenBMP = NULL, *pat2SmpDialogBMP   = NULL, *trackerFrameBMP    = NULL;
-uint32_t *yesNoDialogBMP   = NULL, *bigYesNoDialogBMP  = NULL;
+uint32_t *yesNoDialogBMP   = NULL, *bigYesNoDialogBMP  = NULL, *sampleMonitorBMP   = NULL;
+uint32_t *samplingBoxBMP   = NULL;
 
 void createBitmaps(void)
 {
@@ -94,7 +95,7 @@
 
 	// create spectrum analyzer bar graphics
 	for (i = 0; i < 36; i++)
-		spectrumAnaBMP[i] = RGB12_to_RGB24(analyzerColors[35-i]);
+		analyzerColorsRGB24[i] = RGB12_to_RGB24(analyzerColors[35-i]);
 
 	// create VU-Meter bar graphics
 	for (i = 0; i < 48; i++)
@@ -179,6 +180,8 @@
 	if (aboutScreenBMP != NULL) free(aboutScreenBMP);
 	if (muteButtonsBMP != NULL) free(muteButtonsBMP);
 	if (editOpModeCharsBMP != NULL) free(editOpModeCharsBMP);
+	if (sampleMonitorBMP != NULL) free(sampleMonitorBMP);
+	if (samplingBoxBMP != NULL) free(samplingBoxBMP);
 }
 
 uint32_t *unpackBMP(const uint8_t *src, uint32_t packedLen)
@@ -232,19 +235,19 @@
 	{
 		byteIn = (tmpBuffer[i] & 0xC0) >> 6;
 		assert(byteIn < PALETTE_NUM);
-		dst[(i * 4) + 0] = video.palette[byteIn];
+		dst[(i << 2) + 0] = video.palette[byteIn];
 
 		byteIn = (tmpBuffer[i] & 0x30) >> 4;
 		assert(byteIn < PALETTE_NUM);
-		dst[(i * 4) + 1] = video.palette[byteIn];
+		dst[(i << 2) + 1] = video.palette[byteIn];
 
 		byteIn = (tmpBuffer[i] & 0x0C) >> 2;
 		assert(byteIn < PALETTE_NUM);
-		dst[(i * 4) + 2] = video.palette[byteIn];
+		dst[(i << 2) + 2] = video.palette[byteIn];
 
 		byteIn = (tmpBuffer[i] & 0x03) >> 0;
 		assert(byteIn < PALETTE_NUM);
-		dst[(i * 4) + 3] = video.palette[byteIn];
+		dst[(i << 2) + 3] = video.palette[byteIn];
 	}
 
 	free(tmpBuffer);
@@ -272,6 +275,8 @@
 	aboutScreenBMP = unpackBMP(aboutScreenPackedBMP, sizeof (aboutScreenPackedBMP));
 	muteButtonsBMP = unpackBMP(muteButtonsPackedBMP, sizeof (muteButtonsPackedBMP));
 	editOpModeCharsBMP = unpackBMP(editOpModeCharsPackedBMP, sizeof (editOpModeCharsPackedBMP));
+	sampleMonitorBMP = unpackBMP(sampleMonitorPackedBMP, sizeof (sampleMonitorPackedBMP));
+	samplingBoxBMP = unpackBMP(samplingBoxPackedBMP, sizeof (samplingBoxPackedBMP));
 
 	if (trackerFrameBMP    == NULL || samplerScreenBMP   == NULL || samplerVolumeBMP  == NULL ||
 		clearDialogBMP     == NULL || diskOpScreenBMP    == NULL || mod2wavBMP        == NULL ||
@@ -279,7 +284,7 @@
 		editOpScreen1BMP   == NULL || editOpScreen2BMP   == NULL || editOpScreen3BMP  == NULL ||
 		editOpScreen4BMP   == NULL || aboutScreenBMP     == NULL || muteButtonsBMP    == NULL ||
 		editOpModeCharsBMP == NULL || samplerFiltersBMP  == NULL || yesNoDialogBMP    == NULL ||
-		bigYesNoDialogBMP  == NULL)
+		bigYesNoDialogBMP  == NULL || sampleMonitorBMP   == NULL || samplingBoxBMP    == NULL)
 	{
 		showErrorMsgBox("Out of memory!");
 		return false; // BMPs are free'd in cleanUp()
--- a/src/pt2_bmp.h
+++ b/src/pt2_bmp.h
@@ -29,20 +29,22 @@
 extern const uint8_t mod2wavPackedBMP[607];
 extern const uint8_t muteButtonsPackedBMP[46];
 extern const uint8_t posEdPackedBMP[1375];
+extern const uint8_t sampleMonitorPackedBMP[441];
 extern const uint8_t samplerVolumePackedBMP[706];
 extern const uint8_t samplerFiltersPackedBMP[933];
-extern const uint8_t samplerScreenPackedBMP[3056];
+extern const uint8_t samplerScreenPackedBMP[3076];
 extern const uint8_t spectrumVisualsPackedBMP[2217];
 extern const uint8_t trackerFramePackedBMP[8486];
 extern const uint8_t yesNoDialogPackedBMP[476];
 extern const uint8_t bigYesNoDialogPackedBMP[472];
 extern const uint8_t pat2SmpDialogPackedBMP[520];
+extern const uint8_t samplingBoxPackedBMP[1379];
 
 // these are filled/normalized on init, so no const
 extern uint32_t vuMeterBMP[480];
 extern uint32_t loopPinsBMP[512];
 extern uint32_t samplingPosBMP[64];
-extern uint32_t spectrumAnaBMP[36];
+extern uint32_t analyzerColorsRGB24[36];
 extern uint32_t patternCursorBMP[154];
 extern uint32_t *editOpScreen1BMP;
 extern uint32_t *editOpScreen2BMP;
@@ -63,6 +65,8 @@
 extern uint32_t *muteButtonsBMP;
 extern uint32_t *editOpModeCharsBMP;
 extern uint32_t *pat2SmpDialogBMP;
+extern uint32_t *sampleMonitorBMP;
+extern uint32_t *samplingBoxBMP;
 
 bool unpackBMPs(void);
 void createBitmaps(void);
--- a/src/pt2_config.c
+++ b/src/pt2_config.c
@@ -44,9 +44,9 @@
 	// set default config values first
 	config.fullScreenStretch = false;
 	config.pattDots = false;
-	config.dottedCenterFlag = true;
+	config.waveformCenterLine = true;
 	config.a500LowPassFilter = false;
-	config.soundFrequency = 48000;
+	config.soundFrequency = 96000;
 	config.rememberPlayMode = false;
 	config.stereoSeparation = 20;
 	config.videoScaleFactor = 2;
@@ -65,6 +65,8 @@
 	config.startInFullscreen = false;
 	config.pixelFilter = PIXELFILTER_NEAREST;
 	config.integerScaling = true;
+	config.audioInputFrequency = 44100;
+	config.normalizeSampling = true;
 
 #ifndef _WIN32
 	getcwd(oldCwd, PATH_MAX);
@@ -295,7 +297,10 @@
 		else if (!_strnicmp(configLine, "QUANTIZE=", 9))
 		{
 			if (configLine[9] != '\0')
-				config.quantizeValue = (int16_t)(CLAMP(atoi(&configLine[9]), 0, 63));
+			{
+				const int32_t num = atoi(&configLine[9]);
+				config.quantizeValue = (int16_t)(CLAMP(num, 0, 63));
+			}
 		}
 
 		// TRANSDEL
@@ -308,8 +313,8 @@
 		// DOTTEDCENTER
 		else if (!_strnicmp(configLine, "DOTTEDCENTER=", 13))
 		{
-			     if (!_strnicmp(&configLine[13], "TRUE",  4)) config.dottedCenterFlag = true;
-			else if (!_strnicmp(&configLine[13], "FALSE", 5)) config.dottedCenterFlag = false;
+			     if (!_strnicmp(&configLine[13], "TRUE",  4)) config.waveformCenterLine = true;
+			else if (!_strnicmp(&configLine[13], "FALSE", 5)) config.waveformCenterLine = false;
 		}
 
 		// MODDOT
@@ -384,11 +389,31 @@
 			else if (!_strnicmp(&configLine[19], "FALSE", 5)) config.a500LowPassFilter = false;
 		}
 
+		// SAMPLINGFREQ
+		else if (!_strnicmp(configLine, "SAMPLINGFREQ=", 13))
+		{
+			if (configLine[10] != '\0')
+			{
+				const int32_t num = atoi(&configLine[13]);
+				config.audioInputFrequency = CLAMP(num, 44100, 192000);
+			}
+		}
+
+		// NORMALIZESAMPLING
+		else if (!_strnicmp(configLine, "NORMALIZESAMPLING=", 18))
+		{
+			     if (!_strnicmp(&configLine[18], "TRUE",  4)) config.normalizeSampling = true;
+			else if (!_strnicmp(&configLine[18], "FALSE", 5)) config.normalizeSampling = false;
+		}
+
 		// FREQUENCY
 		else if (!_strnicmp(configLine, "FREQUENCY=", 10))
 		{
 			if (configLine[10] != '\0')
-				config.soundFrequency = (uint32_t)(CLAMP(atoi(&configLine[10]), 32000, 96000));
+			{
+				const int32_t num = atoi(&configLine[10]);
+				config.soundFrequency = CLAMP(num, 44100, 192000);
+			}
 		}
 
 		// BUFFERSIZE
@@ -395,7 +420,10 @@
 		else if (!_strnicmp(configLine, "BUFFERSIZE=", 11))
 		{
 			if (configLine[11] != '\0')
-				config.soundBufferSize = (uint32_t)(CLAMP(atoi(&configLine[11]), 128, 8192));
+			{
+				const int32_t num = atoi(&configLine[11]);
+				config.soundBufferSize = CLAMP(num, 128, 8192);
+			}
 		}
 
 		// STEREOSEPARATION
@@ -402,7 +430,10 @@
 		else if (!_strnicmp(configLine, "STEREOSEPARATION=", 17))
 		{
 			if (configLine[17] != '\0')
-				config.stereoSeparation = (int8_t)(CLAMP(atoi(&configLine[17]), 0, 100));
+			{
+				const int32_t num = atoi(&configLine[17]);
+				config.stereoSeparation = (int8_t)(CLAMP(num, 0, 100));
+			}
 		}
 
 		configLine = strtok(NULL, "\n");
--- a/src/pt2_config.h
+++ b/src/pt2_config.h
@@ -13,13 +13,13 @@
 typedef struct config_t
 {
 	char *defModulesDir, *defSamplesDir;
-	bool dottedCenterFlag, pattDots, a500LowPassFilter, compoMode, autoCloseDiskOp, hideDiskOpDates, hwMouse;
+	bool waveformCenterLine, pattDots, a500LowPassFilter, compoMode, autoCloseDiskOp, hideDiskOpDates, hwMouse;
 	bool transDel, fullScreenStretch, vsyncOff, modDot, blankZeroFlag, realVuMeters, rememberPlayMode;
-	bool sampleLowpass, startInFullscreen, integerScaling;
+	bool sampleLowpass, startInFullscreen, integerScaling, normalizeSampling;
 	int8_t stereoSeparation, videoScaleFactor, accidental;
 	uint8_t pixelFilter;
 	uint16_t quantizeValue;
-	uint32_t soundFrequency, soundBufferSize;
+	uint32_t soundFrequency, soundBufferSize, audioInputFrequency;
 } config_t;
 
 extern config_t config; // pt2_config.c
--- a/src/pt2_diskop.c
+++ b/src/pt2_diskop.c
@@ -8,7 +8,6 @@
 #include <stdint.h>
 #include <stdbool.h>
 #include <math.h>
-#include <ctype.h> // tolower()
 #ifdef _WIN32
 #include <direct.h>
 #include <io.h>
@@ -37,6 +36,7 @@
 #include "pt2_keyboard.h"
 #include "pt2_visuals.h"
 #include "pt2_sample_loader.h"
+#include "pt2_bmp.h"
 
 typedef struct fileEntry_t
 {
@@ -196,6 +196,9 @@
 	}
 #endif
 
+	if (searchRec->filesize < -1)
+		searchRec->filesize = -1;
+
 	if (!listEntry(searchRec))
 	{
 		// skip entry
@@ -273,6 +276,9 @@
 	}
 #endif
 
+	if (searchRec->filesize < -1)
+		searchRec->filesize = -1;
+
 	if (!listEntry(searchRec))
 	{
 		// skip entry
@@ -319,11 +325,9 @@
 
 void handleEntryJumping(SDL_Keycode jumpToChar) // SHIFT+character
 {
-	int32_t i;
-
 	if (diskOpEntry != NULL)
 	{
-		for (i = 0; i < diskop.numEntries; i++)
+		for (int32_t i = 0; i < diskop.numEntries; i++)
 		{
 			if (jumpToChar == diskOpEntry[i].firstAnsiChar)
 			{
@@ -599,7 +603,7 @@
 	if (diskop.numEntries < 2)
 		return; // no need to sort
 
-	offset = diskop.numEntries / 2;
+	offset = diskop.numEntries >> 1;
 	while (offset > 0)
 	{
 		limit = diskop.numEntries - offset;
@@ -637,7 +641,7 @@
 		}
 		while (didSwap);
 
-		offset /= 2;
+		offset >>= 1;
 	}
 }
 
@@ -788,9 +792,8 @@
 {
 	char *entryName;
 	uint8_t maxFilenameChars, maxDirNameChars;
-	uint16_t textXStart, x, y;
-	int32_t i, entryLength;
-	uint32_t *dstPtr;
+	uint16_t x, y, textXStart;
+	int32_t entryLength;
 	fileEntry_t *entry;
 
 	if (config.hideDiskOpDates)
@@ -823,20 +826,13 @@
 	}
 
 	// clear list
-	dstPtr = &video.frameBuffer[(35 * SCREEN_W) + 8];
-	for (y = 0; y < 59; y++)
-	{
-		for (x = 0; x < 295; x++)
-			dstPtr[x] = video.palette[PAL_BACKGRD];
+	fillRect(8, 35, 295, 59, video.palette[PAL_BACKGRD]);
 
-		dstPtr += SCREEN_W;
-	}
-
 	if (diskop.isFilling || diskOpEntry == NULL)
 		return;
 
 	// list entries
-	for (i = 0; i < DISKOP_LINES; i++)
+	for (int32_t i = 0; i < DISKOP_LINES; i++)
 	{
 		if (diskop.scrollOffset+i >= diskop.numEntries)
 			break;
@@ -963,4 +959,73 @@
 void diskOpLoadFile2(void)
 {
 	diskOpLoadFile(oldFileEntryRow, false);
+}
+
+void renderDiskOpScreen(void)
+{
+	blit32(0, 0, 320, 99, diskOpScreenBMP);
+
+	ui.updateDiskOpPathText = true;
+	ui.updatePackText = true;
+	ui.updateSaveFormatText = true;
+	ui.updateLoadMode = true;
+	ui.updateDiskOpFileList = true;
+}
+
+void updateDiskOp(void)
+{
+	char tmpChar;
+
+	if (!ui.diskOpScreenShown || ui.posEdScreenShown)
+		return;
+
+	if (ui.updateDiskOpFileList)
+	{
+		ui.updateDiskOpFileList = false;
+		diskOpRenderFileList();
+	}
+
+	if (ui.updateLoadMode)
+	{
+		ui.updateLoadMode = false;
+
+		// clear boxes
+		fillRect(147,  3, FONT_CHAR_W, FONT_CHAR_H, video.palette[PAL_GENBKG]);
+		fillRect(147, 14, FONT_CHAR_W, FONT_CHAR_H, video.palette[PAL_GENBKG]);
+
+		// draw load mode arrow
+		if (diskop.mode == 0)
+			charOut(147, 3, ARROW_RIGHT, video.palette[PAL_GENTXT]);
+		else
+			charOut(147,14, ARROW_RIGHT, video.palette[PAL_GENTXT]);
+	}
+
+	if (ui.updatePackText)
+	{
+		ui.updatePackText = false;
+		textOutBg(120, 3, diskop.modPackFlg ? "ON " : "OFF", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
+	}
+
+	if (ui.updateSaveFormatText)
+	{
+		ui.updateSaveFormatText = false;
+		     if (diskop.smpSaveType == DISKOP_SMP_WAV) textOutBg(120, 14, "WAV", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
+		else if (diskop.smpSaveType == DISKOP_SMP_IFF) textOutBg(120, 14, "IFF", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
+		else if (diskop.smpSaveType == DISKOP_SMP_RAW) textOutBg(120, 14, "RAW", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
+	}
+
+	if (ui.updateDiskOpPathText)
+	{
+		ui.updateDiskOpPathText = false;
+
+		// print disk op. path
+		for (int32_t i = 0; i < 26; i++)
+		{
+			tmpChar = editor.currPath[ui.diskOpPathTextOffset+i];
+			if (tmpChar == '\0')
+				tmpChar = '_';
+
+			charOutBg(24 + (i * FONT_CHAR_W), 25, tmpChar, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
+		}
+	}
 }
--- a/src/pt2_diskop.h
+++ b/src/pt2_diskop.h
@@ -3,6 +3,7 @@
 #include <stdint.h>
 #include <stdbool.h>
 #include "pt2_header.h"
+
 enum
 {
 	DISKOP_NO_CACHE = 0,
@@ -28,3 +29,5 @@
 void freeDiskOpEntryMem(void);
 void setPathFromDiskOpMode(void);
 bool changePathToHome(void);
+void renderDiskOpScreen(void);
+void updateDiskOp(void);
\ No newline at end of file
--- a/src/pt2_edit.c
+++ b/src/pt2_edit.c
@@ -5,7 +5,6 @@
 
 #include <stdint.h>
 #include <stdbool.h>
-#include <ctype.h> // tolower()
 #include <fcntl.h>
 #include <sys/types.h>
 #include <sys/stat.h>
@@ -28,6 +27,8 @@
 #include "pt2_scopes.h"
 #include "pt2_structs.h"
 #include "pt2_config.h"
+#include "pt2_audio.h"
+#include "pt2_sync.h"
 
 const int8_t scancode2NoteLo[52] = // "USB usage page standard" order
 {
@@ -96,7 +97,6 @@
 void exitGetTextLine(bool updateValue)
 {
 	int8_t tmp8;
-	uint8_t i;
 	int16_t posEdPos, tmp16;
 	int32_t tmp32;
 	UNICHAR *pathU;
@@ -148,12 +148,6 @@
 		if (ui.dstOffset != NULL)
 			*ui.dstOffset = '\0';
 
-		if (ui.editObject == PTB_SONGNAME)
-		{
-			for (i = 0; i < 20; i++)
-				song->header.name[i] = (char)tolower(song->header.name[i]);
-		}
-
 		pointerSetPreviousMode();
 
 		if (!editor.mixFlag)
@@ -639,7 +633,7 @@
 	ui.editObject = editObject;
 
 	if (ui.dstOffset != NULL)
-	   *ui.dstOffset  = '\0';
+		ui.dstOffset[0] = '\0';
 
 	// kludge
 	if (editor.mixFlag)
@@ -905,6 +899,66 @@
 	return false;
 }
 
+void handleSampleJamming(SDL_Scancode scancode) // used for the sampling feature (in SAMPLER)
+{
+	const int32_t ch = cursor.channel;
+
+	if (scancode == SDL_SCANCODE_NONUSBACKSLASH)
+	{
+		turnOffVoices(); // magic "kill all voices" button
+		return;
+	}
+
+	const int8_t noteVal = keyToNote(scancode);
+	if (noteVal < 0 || noteVal > 35)
+		return;
+
+	moduleSample_t *s = &song->samples[editor.currSample];
+	if (s->length <= 1)
+		return;
+
+	song->channels[ch].n_samplenum = editor.currSample; // needed for sample playback/sampling line
+
+	const int8_t *n_start = &song->sampleData[s->offset];
+	const int8_t vol = 64;
+	const uint16_t n_length = s->length >> 1;
+	const uint16_t period = periodTable[((s->fineTune & 0xF) * 37) + noteVal];
+
+	paulaSetVolume(ch, vol);
+	paulaSetPeriod(ch, period);
+	paulaSetData(ch, n_start);
+	paulaSetLength(ch, n_length);
+
+	if (!editor.songPlaying)
+	{
+		scopeSetVolume(ch, vol);
+		scopeSetPeriod(ch, period);
+		scopeSetData(ch, n_start);
+		scopeSetLength(ch, n_length);
+	}
+
+	if (!editor.muted[ch])
+	{
+		paulaStartDMA(ch);
+		if (!editor.songPlaying)
+			scopeTrigger(ch);
+	}
+	else
+	{
+		paulaStopDMA(ch);
+	}
+
+	// these take effect after the current DMA cycle is done
+	paulaSetData(ch, NULL);
+	paulaSetLength(ch, 1);
+
+	if (!editor.songPlaying)
+	{
+		scopeSetData(ch, NULL);
+		scopeSetLength(ch, 1);
+	}
+}
+
 void jamAndPlaceSample(SDL_Scancode scancode, bool normalMode)
 {
 	int8_t noteVal;
@@ -952,14 +1006,34 @@
 			paulaSetData(ch, chn->n_start);
 			paulaSetLength(ch, chn->n_length);
 
+			if (!editor.songPlaying)
+			{
+				scopeSetVolume(ch, chn->n_volume);
+				scopeSetPeriod(ch, chn->n_period);
+				scopeSetData(ch, chn->n_start);
+				scopeSetLength(ch, chn->n_length);
+			}
+
 			if (!editor.muted[ch])
+			{
 				paulaStartDMA(ch);
+				if (!editor.songPlaying)
+					scopeTrigger(ch);
+			}
 			else
+			{
 				paulaStopDMA(ch);
+			}
 
 			// these take effect after the current DMA cycle is done
 			paulaSetData(ch, chn->n_loopstart);
 			paulaSetLength(ch, chn->n_replen);
+
+			if (!editor.songPlaying)
+			{
+				scopeSetData(ch, chn->n_loopstart);
+				scopeSetLength(ch, chn->n_replen);
+			}
 		}
 
 		// normalMode = normal keys, or else keypad keys (in jam mode)
--- a/src/pt2_edit.h
+++ b/src/pt2_edit.h
@@ -24,3 +24,5 @@
 uint8_t handleSpecialKeys(SDL_Scancode scancode);
 int8_t keyToNote(SDL_Scancode scancode);
 void updateTextObject(int16_t editObject);
+
+void handleSampleJamming(SDL_Scancode scancode);
--- a/src/pt2_header.h
+++ b/src/pt2_header.h
@@ -14,7 +14,7 @@
 #include "pt2_unicode.h"
 #include "pt2_palette.h"
 
-#define PROG_VER_STR "1.16"
+#define PROG_VER_STR "1.17"
 
 #ifdef _WIN32
 #define DIR_DELIMITER '\\'
@@ -217,7 +217,7 @@
 void playPattern(int8_t startRow);
 void modPlay(int16_t patt, int16_t order, int8_t row);
 void modSetSpeed(uint8_t speed);
-void modSetTempo(uint16_t bpm);
+void modSetTempo(uint16_t bpm, bool doLockAudio);
 void modFree(void);
 bool setupAudio(void);
 void audioClose(void);
--- a/src/pt2_keyboard.c
+++ b/src/pt2_keyboard.c
@@ -28,6 +28,7 @@
 #include "pt2_mouse.h"
 #include "pt2_unicode.h"
 #include "pt2_config.h"
+#include "pt2_sampling.h"
 
 #if defined _WIN32 && !defined _DEBUG
 extern bool windowsKeyIsDown;
@@ -60,7 +61,6 @@
 	keyb.leftAltPressed = (modState & KMOD_LALT) ? true : false;
 	keyb.shiftPressed = (modState & (KMOD_LSHIFT + KMOD_RSHIFT)) ? true : false;
 
-
 #ifdef __APPLE__
 	keyb.leftCommandPressed = (modState & KMOD_LGUI) ? true : false;
 #endif
@@ -430,7 +430,7 @@
 
 	if (!handleGeneralModes(keycode, scancode)) return;
 	if (!handleTextEditMode(scancode)) return;
-	if (ui.samplerVolBoxShown) return;
+	if (ui.samplerVolBoxShown || ui.samplingBoxShown) return;
 
 	if (ui.samplerFiltersBoxShown)
 	{
@@ -726,11 +726,11 @@
 				if (editor.timingMode == TEMPO_MODE_VBLANK)
 				{
 					editor.oldTempo = song->currBPM;
-					modSetTempo(125);
+					modSetTempo(125, true);
 				}
 				else
 				{
-					modSetTempo(editor.oldTempo);
+					modSetTempo(editor.oldTempo, true);
 				}
 
 				ui.updateSongTiming = true;
@@ -857,12 +857,15 @@
 					statusAllRight();
 				}
 			}
-			else if (!ui.samplerScreenShown)
+			else
 			{
-				modStop();
-				editor.currMode = MODE_EDIT;
-				pointerSetMode(POINTER_MODE_EDIT, DO_CARRY);
-				statusAllRight();
+				if (!ui.samplerScreenShown)
+				{
+					modStop();
+					editor.currMode = MODE_EDIT;
+					pointerSetMode(POINTER_MODE_EDIT, DO_CARRY);
+					statusAllRight();
+				}
 			}
 		}
 		break;
@@ -3037,7 +3040,7 @@
 				}
 				else
 				{
-					modSetTempo(125);
+					modSetTempo(125, true);
 					modSetSpeed(6);
 
 					for (i = 0; i < AMIGA_VOICES; i++)
@@ -3484,6 +3487,70 @@
 		return false;
 	}
 
+	// SAMPLER SCREEN (sampling box)
+	if (ui.samplingBoxShown)
+	{
+		if (audio.isSampling)
+		{
+			stopSampling();
+			return false;
+		}
+
+
+		if (scancode == SDL_SCANCODE_F1)
+			editor.keyOctave = OCTAVE_LOW;
+		else if (scancode == SDL_SCANCODE_F2)
+			editor.keyOctave = OCTAVE_HIGH;
+
+		if (ui.changingSamplingNote)
+		{
+			if (scancode == SDL_SCANCODE_ESCAPE)
+			{
+				ui.changingSamplingNote = false;
+				setPrevStatusMessage();
+				pointerSetPreviousMode();
+			}
+
+			rawKey = keyToNote(scancode);
+			if (rawKey >= 0)
+			{
+				ui.changingSamplingNote = false;
+
+				setSamplingNote(rawKey);
+
+				setPrevStatusMessage();
+				pointerSetPreviousMode();
+			}
+
+			return false;
+		}
+		else
+		{
+			if (keyb.leftCtrlPressed)
+			{
+				if (scancode == SDL_SCANCODE_LEFT)
+					samplingSampleNumDown();
+				else if (scancode == SDL_SCANCODE_RIGHT)
+					samplingSampleNumUp();
+			}
+			else
+			{
+				if (scancode == SDL_SCANCODE_SPACE)
+					turnOffVoices();
+				else
+					handleSampleJamming(scancode);
+			}
+		}
+
+		if (!ui.editTextFlag && scancode == SDL_SCANCODE_ESCAPE)
+		{
+			ui.samplingBoxShown = false;
+			removeSamplingBox();
+		}
+
+		return false;
+	}
+
 	// EDIT OP. SCREEN #3
 	if (editor.mixFlag && scancode == SDL_SCANCODE_ESCAPE)
 	{
@@ -3606,7 +3673,7 @@
 	{
 		if (ui.askScreenShown && ui.askScreenType == ASK_QUIT)
 		{
-			if (keycode == SDLK_y)
+			if (keycode == SDLK_y || keycode == SDLK_RETURN)
 			{
 				ui.askScreenShown = false;
 				ui.answerNo = false;
@@ -3613,7 +3680,7 @@
 				ui.answerYes = true;
 				handleAskYes();
 			}
-			else if (keycode == SDLK_n)
+			else if (keycode == SDLK_n || keycode == SDLK_ESCAPE)
 			{
 				ui.askScreenShown = false;
 				ui.answerNo = true;
--- a/src/pt2_main.c
+++ b/src/pt2_main.c
@@ -35,6 +35,8 @@
 #include "pt2_scopes.h"
 #include "pt2_audio.h"
 #include "pt2_bmp.h"
+#include "pt2_sync.h"
+#include "pt2_sampling.h"
 
 #define CRASH_TEXT "Oh no!\nThe ProTracker 2 clone has crashed...\n\nA backup .mod was hopefully " \
                    "saved to the current module directory.\n\nPlease report this bug if you can.\n" \
@@ -85,6 +87,21 @@
 static void handleSigTerm(void);
 static void cleanUp(void);
 
+static void clearStructs(void)
+{
+	memset(&keyb,   0, sizeof (keyb));
+	memset(&mouse,  0, sizeof (mouse));
+	memset(&video,  0, sizeof (video));
+	memset(&editor, 0, sizeof (editor));
+	memset(&diskop, 0, sizeof (diskop));
+	memset(&cursor, 0, sizeof (cursor));
+	memset(&ui,     0, sizeof (ui));
+	memset(&config, 0, sizeof (config));
+	memset(&audio,  0, sizeof (audio));
+
+	audio.rescanAudioDevicesSupported = true;
+}
+
 int main(int argc, char *argv[])
 {
 #ifndef _WIN32
@@ -121,6 +138,8 @@
 #endif
 #endif
 
+	clearStructs();
+
 	// on Windows and macOS, test what version SDL2.DLL is (against library version used in compilation)
 #if defined _WIN32 || defined __APPLE__
 	SDL_GetVersion(&sdlVer);
@@ -237,6 +256,8 @@
 	SDL_EventState(SDL_SYSWMEVENT, SDL_ENABLE);
 #endif
 
+	setupPerfFreq();
+
 	if (!setupAudio() || !unpackBMPs())
 	{
 		cleanUp();
@@ -245,7 +266,6 @@
 	}
 
 	setupSprites();
-	setupPerfFreq();
 
 	song = createNewMod();
 	if (song == NULL)
@@ -262,7 +282,7 @@
 		return 1;
 	}
 
-	modSetTempo(editor.initialTempo);
+	modSetTempo(editor.initialTempo, false);
 	modSetSpeed(editor.initialSpeed);
 
 	updateWindowTitle(MOD_NOT_MODIFIED);
@@ -307,6 +327,7 @@
 	setupWaitVBL();
 	while (editor.programRunning)
 	{
+		updateChannelSyncBuffer();
 		readMouseXY();
 		readKeyModifiers(); // set/clear CTRL/ALT/SHIFT/AMIGA key states
 		handleInput();
@@ -442,13 +463,6 @@
 
 static bool initializeVars(void)
 {
-	// clear common structs
-	memset(&keyb, 0, sizeof (keyb));
-	memset(&mouse, 0, sizeof (mouse));
-	memset(&video, 0, sizeof (video));
-	memset(&editor, 0, sizeof (editor));
-	memset(&config, 0, sizeof (config));
-
 	setDefaultPalette();
 
 	editor.repeatKeyFlag = (SDL_GetModState() & KMOD_CAPS) ? true : false;
@@ -473,6 +487,7 @@
 	turnOffVoices();
 
 	// set various non-zero values
+	
 	editor.vol1 = 100;
 	editor.vol2 = 100;
 	editor.note1 = 36;
@@ -640,6 +655,7 @@
 		if (audioDriver != NULL && strcmp("directsound", audioDriver) == 0)
 		{
 			SDL_setenv("SDL_AUDIODRIVER", "directsound", true);
+			audio.rescanAudioDevicesSupported = false;
 			break;
 		}
 	}
@@ -653,6 +669,7 @@
 			if (audioDriver != NULL && strcmp("winmm", audioDriver) == 0)
 			{
 				SDL_setenv("SDL_AUDIODRIVER", "winmm", true);
+				audio.rescanAudioDevicesSupported = false;
 				break;
 			}
 		}
@@ -874,6 +891,7 @@
 	freeBMPs();
 	videoClose();
 	freeSprites();
+	freeAudioDeviceList(); // pt2_sampling.c
 
 	if (config.defModulesDir != NULL) free(config.defModulesDir);
 	if (config.defSamplesDir != NULL) free(config.defSamplesDir);
--- a/src/pt2_mod2wav.c
+++ b/src/pt2_mod2wav.c
@@ -178,7 +178,7 @@
 
 	editor.abortMod2Wav = false;
 
-	modSetTempo(song->currBPM); // update BPM with MOD2WAV audio output rate
+	modSetTempo(song->currBPM, true); // update BPM with MOD2WAV audio output rate
 
 	editor.mod2WavThread = SDL_CreateThread(mod2WavThreadFunc, NULL, fOut);
 	if (editor.mod2WavThread != NULL)
--- a/src/pt2_module_loader.c
+++ b/src/pt2_module_loader.c
@@ -26,6 +26,7 @@
 #include "pt2_module_loader.h"
 #include "pt2_sample_loader.h"
 #include "pt2_config.h"
+#include "pt2_sampling.h"
 
 typedef struct mem_t
 {
@@ -944,7 +945,7 @@
 	editor.sampleZero = false;
 	editor.hiLowInstr = 0;
 
-	setLEDFilter(false); // real PT doesn't do this, but that's insane
+	setLEDFilter(false, false); // real PT doesn't do this, but that's insane
 
 	updateWindowTitle(MOD_NOT_MODIFIED);
 
@@ -951,7 +952,7 @@
 	editor.timingMode = TEMPO_MODE_CIA;
 
 	modSetSpeed(6);
-	modSetTempo(song->header.initialTempo); // 125 for normal MODs, custom value for certain STK/UST MODs
+	modSetTempo(song->header.initialTempo, false); // 125 for normal MODs, custom value for certain STK/UST MODs
 
 	updateCurrSample();
 	editor.samplePos = 0;
@@ -1041,7 +1042,8 @@
 
 	// don't allow drag n' drop if the tracker is busy
 	if (ui.pointerMode == POINTER_MODE_MSG1 || diskop.isFilling ||
-		editor.isWAVRendering || ui.samplerFiltersBoxShown || ui.samplerVolBoxShown)
+		editor.isWAVRendering || editor.isSMPRendering ||
+		ui.samplerFiltersBoxShown || ui.samplerVolBoxShown || ui.samplingBoxShown)
 	{
 		return;
 	}
--- a/src/pt2_mouse.c
+++ b/src/pt2_mouse.c
@@ -25,6 +25,7 @@
 #include "pt2_keyboard.h"
 #include "pt2_config.h"
 #include "pt2_bmp.h"
+#include "pt2_sampling.h"
 
 /* TODO: Move irrelevant routines outta here! Disgusting design!
 ** Keep in mind that this was programmed in my early programming days...
@@ -108,6 +109,14 @@
 	}
 }
 
+void pointerResetThreadSafe(void) // used for effect F00 in replayer (stop song)
+{
+	ui.previousPointerMode = ui.pointerMode = POINTER_MODE_IDLE;
+
+	if (config.hwMouse)
+		mouse.resetCursorColorFlag = true;
+}
+
 void pointerSetPreviousMode(void)
 {
 	if (ui.editTextFlag || ui.askScreenShown || ui.clearScreenShown)
@@ -242,6 +251,12 @@
 {
 	int32_t mx, my, windowX, windowY;
 
+	if (mouse.resetCursorColorFlag) // used for effect F00 in replayer (stop song)
+	{
+		mouse.resetCursorColorFlag = false;
+		pointerSetColor(POINTER_GRAY);
+	}
+
 	if (mouse.setPosFlag)
 	{
 		if (!video.windowHidden)
@@ -374,6 +389,7 @@
 
 		mouse.lastGUIButton = -1;
 		mouse.lastSmpFilterButton = -1;
+		mouse.lastSamplingButton = -1;
 	}
 
 	if (mouseButton == SDL_BUTTON_RIGHT)
@@ -419,6 +435,12 @@
 		return;
 	}
 
+	if (ui.samplingBoxShown)
+	{
+		handleRepeatedSamplingButtons();
+		return;
+	}
+
 	if (mouse.lastGUIButton != checkGUIButtons()) // FIXME: This can potentially do a ton of iterations, bad design!
 	{
 		// only repeat the button that was first clicked (e.g. if you hold and move mouse to another button)
@@ -1041,7 +1063,7 @@
 		val = 255;
 
 	song->currBPM = val;
-	modSetTempo(song->currBPM);
+	modSetTempo(song->currBPM, true);
 	ui.updateSongBPM = true;
 }
 
@@ -1062,7 +1084,7 @@
 		val = 32;
 
 	song->currBPM = val;
-	modSetTempo(song->currBPM);
+	modSetTempo(song->currBPM, true);
 	ui.updateSongBPM = true;
 }
 
@@ -2062,7 +2084,7 @@
 
 void mouseWheelUpHandler(void)
 {
-	if (ui.editTextFlag || ui.askScreenShown || ui.clearScreenShown || editor.swapChannelFlag)
+	if (ui.editTextFlag || ui.askScreenShown || ui.clearScreenShown || editor.swapChannelFlag || ui.samplingBoxShown)
 		return;
 
 	if (mouse.y < 121)
@@ -2093,7 +2115,7 @@
 
 void mouseWheelDownHandler(void)
 {
-	if (ui.editTextFlag || ui.askScreenShown || ui.clearScreenShown || editor.swapChannelFlag)
+	if (ui.editTextFlag || ui.askScreenShown || ui.clearScreenShown || editor.swapChannelFlag || ui.samplingBoxShown)
 		return;
 
 	if (mouse.y < 121)
@@ -2167,7 +2189,8 @@
 	{
 		if (!ui.posEdScreenShown && !ui.editOpScreenShown && !ui.diskOpScreenShown &&
 			!ui.aboutScreenShown && !ui.samplerVolBoxShown &&
-			!ui.samplerFiltersBoxShown && !editor.isWAVRendering)
+			!ui.samplerFiltersBoxShown && !ui.samplingBoxShown &&
+			!editor.isWAVRendering)
 		{
 			     if (mouse.x > 127 && mouse.x <= 167) editor.muted[0] ^= 1;
 			else if (mouse.x > 175 && mouse.x <= 215) editor.muted[1] ^= 1;
@@ -2180,7 +2203,7 @@
 
 	// sample hand drawing
 	if (mouse.y >= 138 && mouse.y <= 201 && ui.samplerScreenShown &&
-		!ui.samplerVolBoxShown && !ui.samplerFiltersBoxShown)
+		!ui.samplerVolBoxShown && !ui.samplerFiltersBoxShown && !ui.samplingBoxShown)
 	{
 		samplerEditSample(false);
 	}
@@ -2202,7 +2225,7 @@
 		return true;
 	}
 
-	// handle filters toolbox in sampler
+	// handle filters toolbox in sampler screen
 	else if (ui.samplerFiltersBoxShown)
 	{
 		handleSamplerFiltersBox();
@@ -2209,6 +2232,13 @@
 		return true;
 	}
 
+	// handle sampling toolbox in sampler screen
+	else if (ui.samplingBoxShown)
+	{
+		handleSamplingBox();
+		return true;
+	}
+
 	// "downsample before loading sample" ask dialog
 	if (ui.askScreenShown && ui.askScreenType == ASK_LOAD_DOWNSAMPLE)
 	{
@@ -2238,11 +2268,12 @@
 	}
 
 	// cancel note input gadgets with left/right mouse button
-	if (ui.changingSmpResample || ui.changingChordNote || ui.changingDrumPadNote)
+	if (ui.changingSmpResample || ui.changingChordNote || ui.changingDrumPadNote || ui.changingSamplingNote)
 	{
 		if (mouse.leftButtonPressed || mouse.rightButtonPressed)
 		{
 			ui.changingSmpResample = false;
+			ui.changingSamplingNote = false;
 			ui.changingChordNote = false;
 			ui.changingDrumPadNote = false;
 
@@ -2348,7 +2379,7 @@
 
 	if (editor.errorMsgActive)
 	{
-		if (++editor.errorMsgCounter >= (uint8_t)(VBLANK_HZ/1.25))
+		if (++editor.errorMsgCounter >= (uint8_t)(VBLANK_HZ/1.15))
 		{
 			editor.errorMsgCounter = 0;
 
@@ -2356,7 +2387,7 @@
 			if (!ui.askScreenShown && !ui.clearScreenShown &&
 				!ui.pat2SmpDialogShown && !ui.changingChordNote &&
 				!ui.changingDrumPadNote && !ui.changingSmpResample &&
-				!editor.swapChannelFlag)
+				!editor.swapChannelFlag && !ui.changingSamplingNote)
 			{
 				pointerSetPreviousMode();
 				setPrevStatusMessage();
@@ -4027,12 +4058,10 @@
 		}
 		break;
 
-		case PTB_SA_RESAMPLENOTE:
+		case PTB_SA_SAMPLE:
 		{
-			ui.changingSmpResample = true;
-			ui.updateResampleNote = true;
-			setStatusMessage("SELECT NOTE", NO_CARRY);
-			pointerSetMode(POINTER_MODE_MSG1, NO_CARRY);
+			ui.samplingBoxShown = true;
+			renderSamplingBox();
 		}
 		break;
 
@@ -4043,6 +4072,15 @@
 			pointerSetMode(POINTER_MODE_MSG1, NO_CARRY);
 			setStatusMessage("RESAMPLE?", NO_CARRY);
 			renderAskDialog();
+		}
+		break;
+
+		case PTB_SA_RESAMPLENOTE:
+		{
+			ui.changingSmpResample = true;
+			ui.updateResampleNote = true;
+			setStatusMessage("SELECT NOTE", NO_CARRY);
+			pointerSetMode(POINTER_MODE_MSG1, NO_CARRY);
 		}
 		break;
 
--- a/src/pt2_mouse.h
+++ b/src/pt2_mouse.h
@@ -219,6 +219,7 @@
 	PTB_SA_TUNETONE,
 	PTB_SA_FIXDC,
 	PTB_SA_FILTERS,
+	PTB_SA_SAMPLE,
 	PTB_SA_RESAMPLE,
 	PTB_SA_RESAMPLENOTE,
 	PTB_SA_SAMPLEAREA,
@@ -261,6 +262,7 @@
 void setMsgPointer(void);
 void setErrPointer(void);
 void pointerSetMode(uint8_t pointerMode, bool carry);
+void pointerResetThreadSafe(void); // used for effect F00 in replayer (stop song)
 void pointerSetPreviousMode(void);
 bool setSystemCursor(SDL_Cursor *cur);
 void freeMouseCursors(void);
--- a/src/pt2_pat2smp.c
+++ b/src/pt2_pat2smp.c
@@ -48,7 +48,7 @@
 	editor.blockMarkFlag = false;
 	pointerSetMode(POINTER_MODE_MSG2, NO_CARRY);
 	setStatusMessage("RENDERING...", NO_CARRY);
-	modSetTempo(song->currBPM);
+	modSetTempo(song->currBPM, true);
 	editor.pat2SmpPos = 0;
 
 	editor.smpRenderingDone = false;
@@ -81,11 +81,10 @@
 
 	free(editor.pat2SmpBuf);
 
-	memset(s->text, 0, sizeof (s->text));
 	if (editor.pat2SmpHQ)
 	{
-		strcpy(s->text, "pat2smp (a-3 tune:+5)");
-		s->fineTune = 5;
+		strcpy(s->text, "pat2smp (a-3 tune:+4)");
+		s->fineTune = 4;
 	}
 	else
 	{
--- a/src/pt2_pat2smp.h
+++ b/src/pt2_pat2smp.h
@@ -1,3 +1,11 @@
 #pragma once
 
+#include "pt2_header.h"
+
+#define PAT2SMP_HI_PERIOD 124 /* A-3 finetune +4, 28604.99Hz */
+#define PAT2SMP_LO_PERIOD 160 /* F-3 finetune +1, 22168.09Hz */
+
+#define PAT2SMP_HI_FREQ (PAULA_PAL_CLK / (double)PAT2SMP_HI_PERIOD)
+#define PAT2SMP_LO_FREQ (PAULA_PAL_CLK / (double)PAT2SMP_LO_PERIOD)
+
 void doPat2Smp(void);
--- a/src/pt2_pattern_viewer.c
+++ b/src/pt2_pattern_viewer.c
@@ -6,12 +6,11 @@
 #include "pt2_textout.h"
 #include "pt2_structs.h"
 #include "pt2_config.h"
+#include "pt2_visuals.h"
 
 #define MIDDLE_ROW 7
 #define VISIBLE_ROWS 15
 
-static const char *emptyRowNum = "  ";
-static const char *emptyRowData = "        ";
 static const char emptyDottedEffect[4] = { 0x02, 0x02, 0x02, 0x00 };
 static const char emptyDottedSample[3] = { 0x02, 0x02, 0x00 };
 
@@ -64,11 +63,11 @@
 		if (row < 0 || row >= MOD_ROWS)
 		{
 			// clear empty rows outside of pattern data
-			textOutBg(8, y, emptyRowNum, video.palette[PAL_PATTXT], video.palette[PAL_BACKGRD]);
-			textOutBg(32+(0*72), y, emptyRowData, video.palette[PAL_PATTXT], video.palette[PAL_BACKGRD]);
-			textOutBg(32+(1*72), y, emptyRowData, video.palette[PAL_PATTXT], video.palette[PAL_BACKGRD]);
-			textOutBg(32+(2*72), y, emptyRowData, video.palette[PAL_PATTXT], video.palette[PAL_BACKGRD]);
-			textOutBg(32+(3*72), y, emptyRowData, video.palette[PAL_PATTXT], video.palette[PAL_BACKGRD]);
+			fillRect(8,         y, FONT_CHAR_W*2, FONT_CHAR_H, video.palette[PAL_BACKGRD]);
+			fillRect(32+(0*72), y, FONT_CHAR_W*8, FONT_CHAR_H, video.palette[PAL_BACKGRD]);
+			fillRect(32+(1*72), y, FONT_CHAR_W*8, FONT_CHAR_H, video.palette[PAL_BACKGRD]);
+			fillRect(32+(2*72), y, FONT_CHAR_W*8, FONT_CHAR_H, video.palette[PAL_BACKGRD]);
+			fillRect(32+(3*72), y, FONT_CHAR_W*8, FONT_CHAR_H, video.palette[PAL_BACKGRD]);
 		}
 		else
 		{
@@ -149,11 +148,11 @@
 		if (row < 0 || row >= MOD_ROWS)
 		{
 			// clear empty rows outside of pattern data
-			textOutBg(8, y, emptyRowNum, video.palette[PAL_PATTXT], video.palette[PAL_BACKGRD]);
-			textOutBg(32+(0*72), y, emptyRowData, video.palette[PAL_PATTXT], video.palette[PAL_BACKGRD]);
-			textOutBg(32+(1*72), y, emptyRowData, video.palette[PAL_PATTXT], video.palette[PAL_BACKGRD]);
-			textOutBg(32+(2*72), y, emptyRowData, video.palette[PAL_PATTXT], video.palette[PAL_BACKGRD]);
-			textOutBg(32+(3*72), y, emptyRowData, video.palette[PAL_PATTXT], video.palette[PAL_BACKGRD]);
+			fillRect(8,         y, FONT_CHAR_W*2, FONT_CHAR_H, video.palette[PAL_BACKGRD]);
+			fillRect(32+(0*72), y, FONT_CHAR_W*8, FONT_CHAR_H, video.palette[PAL_BACKGRD]);
+			fillRect(32+(1*72), y, FONT_CHAR_W*8, FONT_CHAR_H, video.palette[PAL_BACKGRD]);
+			fillRect(32+(2*72), y, FONT_CHAR_W*8, FONT_CHAR_H, video.palette[PAL_BACKGRD]);
+			fillRect(32+(3*72), y, FONT_CHAR_W*8, FONT_CHAR_H, video.palette[PAL_BACKGRD]);
 		}
 		else
 		{
--- a/src/pt2_replayer.c
+++ b/src/pt2_replayer.c
@@ -21,6 +21,7 @@
 #include "pt2_visuals.h"
 #include "pt2_textout.h"
 #include "pt2_scopes.h"
+#include "pt2_sync.h"
 
 static bool posJumpAssert, pBreakFlag, updateUIPositions, modHasBeenPlayed;
 static int8_t pBreakPosition, oldRow, modPattern;
@@ -28,19 +29,6 @@
 static int16_t modOrder, oldPattern, oldOrder;
 static uint16_t modBPM, oldBPM;
 
-static const int8_t vuMeterHeights[65] =
-{
-	 0,  0,  1,  2,  2,  3,  4,  5,
-	 5,  6,  7,  8,  8,  9, 10, 11,
-	11, 12, 13, 14, 14, 15, 16, 17,
-	17, 18, 19, 20, 20, 21, 22, 23,
-	23, 24, 25, 26, 26, 27, 28, 29,
-	29, 30, 31, 32, 32, 33, 34, 35,
-	35, 36, 37, 38, 38, 39, 40, 41,
-	41, 42, 43, 44, 44, 45, 46, 47,
-	47
-};
-
 static const uint8_t funkTable[16] = // EFx (FunkRepeat/InvertLoop)
 {
 	0x00, 0x05, 0x06, 0x07, 0x08, 0x0A, 0x0B, 0x0D,
@@ -59,6 +47,7 @@
 	editor.songPlaying = false;
 
 	resetCachedMixerPeriod();
+	resetCachedScopePeriod();
 
 	pattDelTime = 0;
 	pattDelTime2 = 0;
@@ -116,7 +105,11 @@
 	if (vol > 64)
 		vol = 64;
 
-	editor.vuMeterVolumes[ch->n_chanindex] = vuMeterHeights[vol];
+	ch->syncVuVolume = vol;
+	ch->syncFlags |= UPDATE_VUMETER;
+
+	if (!editor.songPlaying)
+		editor.vuMeterVolumes[ch->n_chanindex] = vuMeterHeights[vol];
 }
 
 static void updateFunk(moduleChannel_t *ch)
@@ -213,7 +206,10 @@
 	paulaSetData(ch->n_chanindex, ch->n_loopstart);
 	paulaSetLength(ch->n_chanindex, ch->n_replen);
 
-	updateSpectrumAnalyzer(ch->n_volume, ch->n_period);
+	ch->syncAnalyzerVolume = ch->n_volume;
+	ch->syncAnalyzerPeriod = ch->n_period;
+	ch->syncFlags |= UPDATE_ANALYZER;
+
 	setVUMeterHeight(ch);
 }
 
@@ -335,7 +331,7 @@
 		editor.playMode = PLAY_MODE_NORMAL;
 		editor.currMode = MODE_IDLE;
 
-		pointerSetMode(POINTER_MODE_IDLE, DO_CARRY);
+		pointerResetThreadSafe(); // set gray mouse cursor
 	}
 }
 
@@ -401,7 +397,7 @@
 
 static void filterOnOff(moduleChannel_t *ch)
 {
-	setLEDFilter(!(ch->n_cmd & 1));
+	setLEDFilter(!(ch->n_cmd & 1), false);
 }
 
 static void finePortaUp(moduleChannel_t *ch)
@@ -788,7 +784,11 @@
 		if (!editor.muted[ch->n_chanindex])
 		{
 			paulaStartDMA(ch->n_chanindex);
-			updateSpectrumAnalyzer(ch->n_volume, ch->n_period);
+
+			ch->syncAnalyzerVolume = ch->n_volume;
+			ch->syncAnalyzerPeriod = ch->n_period;
+			ch->syncFlags |= UPDATE_ANALYZER;
+
 			setVUMeterHeight(ch);
 		}
 		else
@@ -987,7 +987,7 @@
 	// PT quirk: CIA refreshes its timer values on the next interrupt, so do the real tempo change here
 	if (setBPMFlag != 0)
 	{
-		modSetTempo(setBPMFlag);
+		modSetTempo(setBPMFlag, false);
 		setBPMFlag = 0;
 	}
 
@@ -1156,7 +1156,7 @@
 		ui.updateStatusText = true;
 }
 
-void modSetTempo(uint16_t bpm)
+void modSetTempo(uint16_t bpm, bool doLockAudio)
 {
 	uint32_t smpsPerTick;
 
@@ -1164,7 +1164,8 @@
 		return;
 
 	const bool audioWasntLocked = !audio.locked;
-	if (audioWasntLocked)
+
+	if (doLockAudio && audioWasntLocked)
 		lockAudio();
 
 	modBPM = bpm;
@@ -1185,7 +1186,14 @@
 
 	mixerSetSamplesPerTick(smpsPerTick);
 
-	if (audioWasntLocked)
+	// calculate tick time length for audio/video sync timestamp
+	const uint64_t tickTimeLen64 = audio.tickTimeLengthTab[bpm];
+	const uint32_t tickTimeLen = tickTimeLen64 >> 32;
+	const uint32_t tickTimeLenFrac = tickTimeLen64 & 0xFFFFFFFF;
+
+	setSyncTickTimeLen(tickTimeLen, tickTimeLenFrac);
+
+	if (doLockAudio && audioWasntLocked)
 		unlockAudio();
 }
 
@@ -1380,10 +1388,10 @@
 	editor.currPatternDisp = &song->header.order[0];
 	editor.currPosEdPattDisp = &song->header.order[0];
 
-	modSetTempo(editor.initialTempo);
+	modSetTempo(editor.initialTempo, true);
 	modSetSpeed(editor.initialSpeed);
 
-	setLEDFilter(false); // real PT doesn't do this there, but that's insane
+	setLEDFilter(false, true); // real PT doesn't do this there, but that's insane
 	updateCurrSample();
 
 	ui.updateSongSize = true;
@@ -1486,7 +1494,7 @@
 		song->currSpeed = 6;
 		song->currBPM = 125;
 		modSetSpeed(6);
-		modSetTempo(125);
+		modSetTempo(125, true);
 
 		modPlay(DONT_SET_PATTERN, 0, 0);
 	}
@@ -1526,7 +1534,7 @@
 	editor.currPosEdPattDisp = &song->header.order[song->currOrder];
 
 	modSetSpeed(oldSpeed);
-	modSetTempo(oldBPM);
+	modSetTempo(oldBPM, true);
 
 	doStopIt(true);
 
--- a/src/pt2_sample_loader.c
+++ b/src/pt2_sample_loader.c
@@ -7,7 +7,6 @@
 #include <string.h>
 #include <stdint.h>
 #include <stdbool.h>
-#include <ctype.h> // tolower()
 #include "pt2_header.h"
 #include "pt2_textout.h"
 #include "pt2_mouse.h"
@@ -18,7 +17,12 @@
 #include "pt2_helpers.h"
 #include "pt2_unicode.h"
 #include "pt2_config.h"
+#include "pt2_sampling.h"
 
+/* TODO: Get a low-pass filter with a steeper slope!
+** A 6db/oct filter may not be very suitable for filtering out frequencies above nyquist,
+** before 2x downsampling.
+*/
 #define DOWNSAMPLE_CUTOFF_FACTOR 4.0
 
 enum
@@ -842,7 +846,7 @@
 		for (i = 0; i < 21; i++)
 		{
 			if (i < inamLen)
-				s->text[i] = (char)tolower(fgetc(f));
+				s->text[i] = (char)fgetc(f);
 			else
 				s->text[i] = '\0';
 		}
@@ -861,7 +865,7 @@
 	{
 		nameLen = (uint32_t)strlen(entryName);
 		for (i = 0; i < 21; i++)
-			s->text[i] = (i < nameLen) ? (char)tolower(entryName[i]) : '\0';
+			s->text[i] = (i < nameLen) ? (char)entryName[i] : '\0';
 
 		s->text[21] = '\0';
 		s->text[22] = '\0';
@@ -876,6 +880,14 @@
 	editor.samplePos = 0;
 
 	fixSampleBeep(s);
+	fillSampleRedoBuffer(editor.currSample);
+
+	if (ui.samplingBoxShown)
+	{
+		removeSamplingBox();
+		ui.samplingBoxShown = false;
+	}
+
 	updateCurrSample();
 
 	updateWindowTitle(MOD_IS_MODIFIED);
@@ -1124,8 +1136,7 @@
 		if (nameLen > 21)
 			nameLen = 21;
 
-		for (i = 0; i < nameLen; i++)
-			s->text[i] = (char)tolower(tmpCharBuf[i]);
+		memcpy(s->text, tmpCharBuf, nameLen);
 	}
 	else
 	{
@@ -1133,8 +1144,7 @@
 		if (nameLen > 21)
 			nameLen = 21;
 
-		for (i = 0; i < nameLen; i++)
-			s->text[i] = (char)tolower(entryName[i]);
+		memcpy(s->text, entryName, nameLen);
 	}
 
 	// remove .iff from end of sample name (if present)
@@ -1146,6 +1156,7 @@
 	editor.samplePos = 0;
 
 	fixSampleBeep(s);
+	fillSampleRedoBuffer(editor.currSample);
 	updateCurrSample();
 
 	updateWindowTitle(MOD_IS_MODIFIED);
@@ -1194,7 +1205,7 @@
 	// copy over sample name
 	nameLen = (uint32_t)strlen(entryName);
 	for (i = 0; i < 21; i++)
-		s->text[i] = (i < nameLen) ? (char)tolower(entryName[i]) : '\0';
+		s->text[i] = (i < nameLen) ? (char)entryName[i] : '\0';
 
 	s->text[21] = '\0';
 	s->text[22] = '\0';
@@ -1203,6 +1214,14 @@
 	editor.samplePos = 0;
 
 	fixSampleBeep(s);
+	fillSampleRedoBuffer(editor.currSample);
+
+	if (ui.samplingBoxShown)
+	{
+		removeSamplingBox();
+		ui.samplingBoxShown = false;
+	}
+
 	updateCurrSample();
 
 	updateWindowTitle(MOD_IS_MODIFIED);
@@ -1675,7 +1694,7 @@
 	// copy over sample name
 	nameLen = (uint32_t)strlen(entryName);
 	for (i = 0; i < 21; i++)
-		s->text[i] = (i < nameLen) ? (char)tolower(entryName[i]) : '\0';
+		s->text[i] = (i < nameLen) ? (char)entryName[i] : '\0';
 
 	s->text[21] = '\0';
 	s->text[22] = '\0';
@@ -1689,6 +1708,14 @@
 	editor.samplePos = 0;
 
 	fixSampleBeep(s);
+	fillSampleRedoBuffer(editor.currSample);
+
+	if (ui.samplingBoxShown)
+	{
+		removeSamplingBox();
+		ui.samplingBoxShown = false;
+	}
+
 	updateCurrSample();
 
 	updateWindowTitle(MOD_IS_MODIFIED);
--- a/src/pt2_sample_saver.c
+++ b/src/pt2_sample_saver.c
@@ -12,7 +12,7 @@
 #include "pt2_helpers.h"
 #include "pt2_diskop.h"
 
-#define PLAYBACK_FREQ 16574 /* C-3 */
+#define PLAYBACK_FREQ 16574 /* C-3, period 214 */
 
 static void removeSampleFileExt(char *text) // for sample saver
 {
--- a/src/pt2_sampler.c
+++ b/src/pt2_sampler.c
@@ -21,6 +21,7 @@
 #include "pt2_structs.h"
 #include "pt2_config.h"
 #include "pt2_bmp.h"
+#include "pt2_sync.h"
 
 #define CENTER_LINE_COLOR 0x303030
 #define MARK_COLOR_1 0x666666 /* inverted background */
@@ -200,7 +201,7 @@
 	}
 }
 
-static void sampleLine(int32_t line_x1, int32_t line_x2, int32_t line_y1, int32_t line_y2)
+void sampleLine(int32_t line_x1, int32_t line_x2, int32_t line_y1, int32_t line_y2)
 {
 	int32_t d, x, y, ax, ay, sx, sy, dx, dy;
 	uint32_t color = 0x01000000 | video.palette[PAL_QADSCP];
@@ -219,7 +220,7 @@
 
 	if (ax > ay)
 	{
-		d = ay - ((uint16_t)ax / 2);
+		d = ay - ((uint16_t)ax >> 1);
 		while (true)
 		{
 			assert(y >= 0 || x >= 0 || y < SCREEN_H || x < SCREEN_W);
@@ -241,7 +242,7 @@
 	}
 	else
 	{
-		d = ax - ((uint16_t)ay / 2);
+		d = ax - ((uint16_t)ay >> 1);
 		while (true)
 		{
 			assert(y >= 0 || x >= 0 || y < SCREEN_H || x < SCREEN_W);
@@ -266,20 +267,20 @@
 static void setDragBar(void)
 {
 	int32_t pos;
-	uint32_t *dstPtr, pixel, bgPixel;
 
+	// clear drag bar background
+	fillRect(4, 206, 312, 4, video.palette[PAL_BACKGRD]);
+
 	if (sampler.samLength > 0 && sampler.samDisplay != sampler.samLength)
 	{
-		int32_t roundingBias = sampler.samLength >> 1;
+		const int32_t roundingBias = sampler.samLength >> 1;
 
 		// update drag bar coordinates
-		pos = ((sampler.samOffset * 311) + roundingBias) / sampler.samLength;
-		sampler.dragStart = (uint16_t)(pos + 4);
-		sampler.dragStart = CLAMP(sampler.dragStart, 4, 315);
+		pos = 4 + (((sampler.samOffset * 311) + roundingBias) / sampler.samLength);
+		sampler.dragStart = (uint16_t)CLAMP(pos, 4, 315);
 
-		pos = (((sampler.samDisplay + sampler.samOffset) * 311) + roundingBias) / sampler.samLength;
-		sampler.dragEnd = (uint16_t)(pos + 5);
-		sampler.dragEnd = CLAMP(sampler.dragEnd, 5, 316);
+		pos = 5 + ((((sampler.samDisplay + sampler.samOffset) * 311) + roundingBias) / sampler.samLength);
+		sampler.dragEnd = (uint16_t)CLAMP(pos, 5, 316);
 
 		if (sampler.dragStart > sampler.dragEnd-1)
 			sampler.dragStart = sampler.dragEnd-1;
@@ -286,38 +287,10 @@
 
 		// draw drag bar
 
-		dstPtr = &video.frameBuffer[206 * SCREEN_W];
-		pixel = video.palette[PAL_QADSCP];
-		bgPixel = video.palette[PAL_BACKGRD];
-
-		for (int32_t y = 0; y < 4; y++)
-		{
-			for (int32_t x = 4; x < 316; x++)
-			{
-				if (x >= sampler.dragStart && x <= sampler.dragEnd)
-					dstPtr[x] = pixel; // drag bar
-				else
-					dstPtr[x] = bgPixel; // background
-			}
-
-			dstPtr += SCREEN_W;
-		}
+		const uint32_t dragWidth = sampler.dragEnd - sampler.dragStart;
+		if (dragWidth > 0)
+			fillRect(sampler.dragStart, 206, dragWidth, 4, video.palette[PAL_QADSCP]);
 	}
-	else
-	{
-		// clear drag bar background
-
-		dstPtr = &video.frameBuffer[(206 * SCREEN_W) + 4];
-		pixel = video.palette[PAL_BACKGRD];
-
-		for (int32_t y = 0; y < 4; y++)
-		{
-			for (int32_t x = 0; x < 312; x++)
-				dstPtr[x] = pixel;
-
-			dstPtr += SCREEN_W;
-		}
-	}
 }
 
 static int8_t getScaledSample(int32_t index)
@@ -382,32 +355,22 @@
 	*outMax = SAMPLE_AREA_Y_CENTER - (smpMax >> 2);
 }
 
-static void renderSampleData(void)
+void renderSampleData(void)
 {
 	int8_t *smpPtr;
 	int16_t y1, y2, min, max, oldMin, oldMax;
-	int32_t x, y, smpIdx, smpNum;
-	uint32_t *dstPtr, pixel;
+	int32_t x, smpIdx, smpNum;
+	uint32_t *dstPtr;
 	moduleSample_t *s;
 
 	s = &song->samples[editor.currSample];
 
 	// clear sample data background
+	fillRect(3, 138, SAMPLE_AREA_WIDTH, SAMPLE_VIEW_HEIGHT, video.palette[PAL_BACKGRD]);
 
-	dstPtr = &video.frameBuffer[(138 * SCREEN_W) + 3];
-	pixel = video.palette[PAL_BACKGRD];
-
-	for (y = 0; y < SAMPLE_VIEW_HEIGHT; y++)
+	// display center line (if enabled)
+	if (config.waveformCenterLine)
 	{
-		for (x = 0; x < SAMPLE_AREA_WIDTH; x++)
-			dstPtr[x] = pixel;
-
-		dstPtr += SCREEN_W;
-	}
-
-	// display center line
-	if (config.dottedCenterFlag)
-	{
 		dstPtr = &video.frameBuffer[(SAMPLE_AREA_Y_CENTER * SCREEN_W) + 3];
 		for (x = 0; x < SAMPLE_AREA_WIDTH; x++)
 			dstPtr[x] = 0x02000000 | CENTER_LINE_COLOR;
@@ -464,6 +427,9 @@
 		}
 	}
 
+	if (ui.samplingBoxShown)
+		return;
+
 	// render "sample display" text
 	if (sampler.samStart == sampler.blankSample)
 		printFiveDecimalsBg(272, 214, 0, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
@@ -552,10 +518,6 @@
 
 		ui.update9xxPos = true;
 		ui.lastSampleOffset = 0x900;
-
-		// for quadrascope
-		sampler.samDrawStart = s->offset;
-		sampler.samDrawEnd = s->offset + s->length;
 	}
 }
 
@@ -1527,18 +1489,24 @@
 	{
 		// turn tuning tone on
 
-		editor.tuningChan = (cursor.channel + 1) & 3;
+		const int8_t ch = editor.tuningChan = (cursor.channel + 1) & 3;
 
 		if (editor.tuningNote > 35)
 			editor.tuningNote = 35;
 
-		song->channels[editor.tuningChan].n_volume = 64; // we need this for the scopes
+		song->channels[ch].n_volume = 64; // we need this for the scopes
 
-		paulaSetPeriod(editor.tuningChan, periodTable[editor.tuningNote]);
-		paulaSetVolume(editor.tuningChan, 64);
-		paulaSetData(editor.tuningChan, tuneToneData);
-		paulaSetLength(editor.tuningChan, sizeof (tuneToneData) / 2);
-		paulaStartDMA(editor.tuningChan);
+		paulaSetPeriod(ch, periodTable[editor.tuningNote]);
+		paulaSetVolume(ch, 64);
+		paulaSetData(ch, tuneToneData);
+		paulaSetLength(ch, sizeof (tuneToneData) / 2);
+		paulaStartDMA(ch);
+
+		scopeSetPeriod(ch, periodTable[editor.tuningNote]);
+		scopeSetVolume(ch, 64);
+		scopeSetData(ch, tuneToneData);
+		scopeSetLength(ch, sizeof (tuneToneData) / 2);
+		scopeTrigger(ch);
 	}
 	else
 	{
@@ -2048,9 +2016,9 @@
 	if (playWaveformFlag)
 	{
 		ch->n_start = &song->sampleData[s->offset];
-		ch->n_length = (s->loopStart > 0) ? (uint32_t)(s->loopStart + s->loopLength) / 2 : s->length / 2;
+		ch->n_length = (s->loopStart > 0) ? (uint32_t)(s->loopStart + s->loopLength) >> 1 : s->length >> 1;
 		ch->n_loopstart = &song->sampleData[s->offset + s->loopStart];
-		ch->n_replen = s->loopLength / 2;
+		ch->n_replen = s->loopLength >> 1;
 	}
 	else
 	{
@@ -2068,10 +2036,24 @@
 	paulaSetData(chn, ch->n_start);
 	paulaSetLength(chn, ch->n_length);
 
+	if (!editor.songPlaying)
+	{
+		scopeSetVolume(chn, ch->n_volume);
+		scopeSetPeriod(chn, ch->n_period);
+		scopeSetData(chn, ch->n_start);
+		scopeSetLength(chn, ch->n_length);
+	}
+
 	if (!editor.muted[chn])
+	{
 		paulaStartDMA(chn);
+		if (!editor.songPlaying)
+			scopeTrigger(chn);
+	}
 	else
+	{
 		paulaStopDMA(chn);
+	}
 
 	// these take effect after the current DMA cycle is done
 	if (playWaveformFlag)
@@ -2078,11 +2060,23 @@
 	{
 		paulaSetData(chn, ch->n_loopstart);
 		paulaSetLength(chn, ch->n_replen);
+
+		if (!editor.songPlaying)
+		{
+			scopeSetData(chn, ch->n_loopstart);
+			scopeSetLength(chn, ch->n_replen);
+		}
 	}
 	else
 	{
 		paulaSetData(chn, NULL);
 		paulaSetLength(chn, 1);
+
+		if (!editor.songPlaying)
+		{
+			scopeSetData(chn, NULL);
+			scopeSetLength(chn, 1);
+		}
 	}
 
 	updateSpectrumAnalyzer(ch->n_volume, ch->n_period);
@@ -2171,7 +2165,7 @@
 void samplerShowAll(void)
 {
 	if (sampler.samDisplay == sampler.samLength)
-		return; // don't attempt to show all if already showing all! }
+		return; // don't attempt to show all if already showing all!
 
 	sampler.samOffset = 0;
 	sampler.samDisplay = sampler.samLength;
--- a/src/pt2_sampler.h
+++ b/src/pt2_sampler.h
@@ -12,11 +12,13 @@
 	uint16_t dragStart, dragEnd;
 	int32_t samPointWidth, samOffset, samDisplay, samLength, saveMouseX, lastSamPos;
 	int32_t lastMouseX, lastMouseY, tmpLoopStart, tmpLoopLength;
-	uint32_t copyBufSize, samDrawStart, samDrawEnd;
+	uint32_t copyBufSize;
 } sampler_t;
 
 extern sampler_t sampler; // pt2_sampler.c
 
+void sampleLine(int32_t line_x1, int32_t line_x2, int32_t line_y1, int32_t line_y2);
+
 void downSample(void);
 void upSample(void);
 void createSampleMarkTable(void);
@@ -60,6 +62,7 @@
 void samplerScreen(void);
 void displaySample(void);
 void redrawSample(void);
+void renderSampleData(void);
 bool allocSamplerVars(void);
 void deAllocSamplerVars(void);
 void setLoopSprites(void);
--- /dev/null
+++ b/src/pt2_sampling.c
@@ -1,0 +1,902 @@
+/* Experimental audio sampling support.
+** There may be several bad practices here, as I don't really
+** have the proper knowledge on this stuff.
+*/
+
+// for finding memory leaks in debug mode with Visual Studio 
+#if defined _DEBUG && defined _MSC_VER
+#include <crtdbg.h>
+#endif
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include "pt2_header.h"
+#include "pt2_textout.h"
+#include "pt2_mouse.h"
+#include "pt2_structs.h"
+#include "pt2_sampler.h" // fixSampleBeep() / sampleLine()
+#include "pt2_visuals.h"
+#include "pt2_helpers.h"
+#include "pt2_bmp.h"
+#include "pt2_unicode.h"
+#include "pt2_audio.h"
+#include "pt2_tables.h"
+#include "pt2_config.h"
+#include "pt2_sinc.h"
+#include "pt2_sampling.h"
+
+enum
+{
+	SAMPLE_LEFT = 0,
+	SAMPLE_RIGHT = 1,
+	SAMPLE_MIX  = 2
+};
+
+// this may change after opening the audio input device
+#define SAMPLING_BUFFER_SIZE 1024
+
+#define FRAC_BITS 24
+#define FRAC_SCALE (1L << 24)
+#define FRAC_MASK (FRAC_SCALE-1)
+
+#define SAMPLE_PREVIEW_WITDH 194
+#define SAMPLE_PREVIEW_HEIGHT 38
+
+#define MAX_INPUT_DEVICES 99
+#define VISIBLE_LIST_ENTRIES 4
+
+static volatile bool callbackBusy, displayingBuffer, samplingEnded;
+static bool audioDevOpen;
+static char *audioInputDevs[MAX_INPUT_DEVICES];
+static uint8_t samplingNote = 33, samplingFinetune = 4; // period 124, max safe period for PAL Paula
+static int16_t displayBuffer[SAMPLING_BUFFER_SIZE], *bufferOrig, *buffer;
+static int32_t samplingMode = SAMPLE_MIX, inputFrequency, roundedOutputFrequency;
+static int32_t numAudioInputDevs, audioInputDevListOffset, selectedDev;
+static int32_t bytesSampled, maxSamplingLength, inputBufferSize;
+static float fOutputFrequency;
+static double dOutputFrequency;
+static SDL_AudioDeviceID recordDev;
+
+static void listAudioDevices(void);
+
+static void updateOutputFrequency(void)
+{
+	if (samplingNote > 35)
+		samplingNote = 35;
+
+	int32_t period = periodTable[((samplingFinetune & 0xF) * 37) + samplingNote];
+	if (period < 113) // this happens internally in our Paula mixer
+		period = 113;
+
+	dOutputFrequency = (double)PAULA_PAL_CLK / period;
+	fOutputFrequency = (float)dOutputFrequency;
+	roundedOutputFrequency = (int32_t)(fOutputFrequency + 0.5f);
+}
+
+static void SDLCALL samplingCallback(void *userdata, Uint8 *stream, int len)
+{
+	callbackBusy = true;
+
+	if (!displayingBuffer)
+	{
+		if (len > SAMPLING_BUFFER_SIZE)
+			len = SAMPLING_BUFFER_SIZE;
+
+		const int16_t *L = (int16_t *)stream;
+		const int16_t *R = ((int16_t *)stream) + 1;
+
+		int16_t *dst16 = displayBuffer;
+
+		if (samplingMode == SAMPLE_LEFT)
+		{
+			for (int32_t i = 0; i < len; i++)
+				dst16[i] = L[i << 1];
+		}
+		else if (samplingMode == SAMPLE_RIGHT)
+		{
+			for (int32_t i = 0; i < len; i++)
+				dst16[i] = R[i << 1];
+		}
+		else
+		{
+			for (int32_t i = 0; i < len; i++)
+				dst16[i] = (L[i << 1] + R[i << 1]) >> 1;
+		}
+	}
+
+	if (audio.isSampling)
+	{
+		if (bytesSampled+len > maxSamplingLength)
+			len = maxSamplingLength - bytesSampled;
+
+		if (len > inputBufferSize)
+			len = inputBufferSize;
+
+		const int16_t *L = (int16_t *)stream;
+		const int16_t *R = ((int16_t *)stream) + 1;
+
+		int16_t *dst16 = &buffer[bytesSampled];
+
+		if (samplingMode == SAMPLE_LEFT)
+		{
+			for (int32_t i = 0; i < len; i++)
+				dst16[i] = L[i << 1];
+		}
+		else if (samplingMode == SAMPLE_RIGHT)
+		{
+			for (int32_t i = 0; i < len; i++)
+				dst16[i] = R[i << 1];
+		}
+		else
+		{
+			for (int32_t i = 0; i < len; i++)
+				dst16[i] = (L[i << 1] + R[i << 1]) >> 1;
+		}
+
+		bytesSampled += len;
+		if (bytesSampled >= maxSamplingLength)
+		{
+			audio.isSampling = true;
+			samplingEnded = true;
+		}
+	}
+
+	callbackBusy = false;
+	(void)userdata;
+}
+
+static void stopInputAudio(void)
+{
+	if (recordDev > 0)
+	{
+		SDL_CloseAudioDevice(recordDev);
+		recordDev = 0;
+	}
+}
+
+static void startInputAudio(void)
+{
+	SDL_AudioSpec want, have;
+
+	if (recordDev > 0)
+		stopInputAudio();
+
+	if (numAudioInputDevs == 0 || selectedDev >= numAudioInputDevs)
+	{
+		audioDevOpen = false;
+		return;
+	}
+
+	assert(roundedOutputFrequency > 0);
+
+	memset(&want, 0, sizeof (SDL_AudioSpec));
+	want.freq = config.audioInputFrequency;
+	want.format = AUDIO_S16;
+	want.channels = 2;
+	want.callback = samplingCallback;
+	want.userdata = NULL;
+	want.samples = SAMPLING_BUFFER_SIZE;
+
+	recordDev = SDL_OpenAudioDevice(audioInputDevs[selectedDev], true, &want, &have, 0);
+	audioDevOpen = (recordDev != 0);
+
+	inputFrequency = have.freq;
+	inputBufferSize = have.samples;
+
+	SDL_PauseAudioDevice(recordDev, false);
+}
+
+static void changeStatusText(const char *text)
+{
+	fillRect(88, 127, 17*FONT_CHAR_W, FONT_CHAR_H, video.palette[PAL_GENBKG]);
+	textOut(88, 127, text, video.palette[PAL_GENTXT]);
+}
+
+static void selectAudioDevice(int32_t dev)
+{
+	if (dev < 0)
+		return;
+
+	if (numAudioInputDevs == 0)
+	{
+		listAudioDevices();
+		return;
+	}
+
+	if (dev >= numAudioInputDevs)
+		return;
+
+	listAudioDevices();
+	changeStatusText("PLEASE WAIT ...");
+	flipFrame();
+
+	stopInputAudio();
+	selectedDev = dev;
+	listAudioDevices();
+	startInputAudio();
+
+	changeStatusText(ui.statusMessage);
+}
+
+void renderSampleMonitor(void)
+{
+	blit32(120, 44, 200, 55, sampleMonitorBMP);
+	memset(displayBuffer, 0, sizeof (displayBuffer));
+}
+
+void freeAudioDeviceList(void)
+{
+	for (int32_t i = 0; i < numAudioInputDevs; i++)
+	{
+		if (audioInputDevs[i] != NULL)
+		{
+			free(audioInputDevs[i]);
+			audioInputDevs[i] = NULL;
+		}
+	}
+}
+
+static void scanAudioDevices(void)
+{
+	freeAudioDeviceList();
+
+	numAudioInputDevs = SDL_GetNumAudioDevices(true);
+	if (numAudioInputDevs > MAX_INPUT_DEVICES)
+		numAudioInputDevs = MAX_INPUT_DEVICES;
+
+	for (int32_t i = 0; i < numAudioInputDevs; i++)
+	{
+		const char *deviceName = SDL_GetAudioDeviceName(i, true);
+		if (deviceName == NULL)
+		{
+			numAudioInputDevs--; // hide device
+			continue;
+		}
+
+		const uint32_t stringLen = (uint32_t)strlen(deviceName);
+
+		audioInputDevs[i] = (char *)malloc(stringLen + 2);
+		if (audioInputDevs[i] == NULL)
+			break;
+
+		if (stringLen > 0)
+			strcpy(audioInputDevs[i], deviceName);
+
+		audioInputDevs[i][stringLen+1] = '\0'; // UTF-8 needs double null termination (XXX: citation needed)
+	}
+
+	audioInputDevListOffset = 0; // reset scroll position
+
+	if (selectedDev >= numAudioInputDevs)
+		selectedDev = 0;
+}
+
+static void listAudioDevices(void)
+{
+	fillRect(3, 219, 163, 33, PAL_BACKGRD);
+
+	if (numAudioInputDevs == 0)
+	{
+		textOut(16, 219+13, "NO DEVICES FOUND!", video.palette[PAL_QADSCP]);
+		return;
+	}
+
+	for (int32_t i = 0; i < VISIBLE_LIST_ENTRIES; i++)
+	{
+		const int32_t dev = audioInputDevListOffset+i;
+		if (audioInputDevListOffset+i >= numAudioInputDevs)
+			break;
+
+		if (dev == selectedDev)
+			fillRect(4, 219+1+(i*(FONT_CHAR_H+3)), 161, 8, video.palette[PAL_GENBKG2]);
+
+		if (audioInputDevs[dev] != NULL)
+			textOutTightN(2+2, 219+2+(i*(FONT_CHAR_H+3)), audioInputDevs[dev], 23, video.palette[PAL_QADSCP]);
+	}
+}
+
+static void drawSamplingNote(void)
+{
+	assert(samplingNote < 36);
+	const char *str = config.accidental ? noteNames2[2+samplingNote]: noteNames1[2+samplingNote];
+	textOutBg(262, 230, str, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
+}
+
+static void drawSamplingFinetune(void)
+{
+	textOutBg(254, 219, ftuneStrTab[samplingFinetune & 0xF], video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
+}
+
+static void drawSamplingFrequency(void)
+{
+	char str[16];
+	sprintf(str, "%05dHZ", roundedOutputFrequency);
+	textOutBg(262, 208, str, roundedOutputFrequency <= 28604 ? video.palette[PAL_GENTXT] : 0x8C0F0F, video.palette[PAL_GENBKG]);
+}
+
+static void drawSamplingModeCross(void)
+{
+	// clear old crosses
+	fillRect(4, 208, 6, 5, video.palette[PAL_GENBKG]);
+	fillRect(51, 208, 6, 5, video.palette[PAL_GENBKG]);
+	fillRect(105, 208, 6, 5, video.palette[PAL_GENBKG]);
+
+	int16_t x;
+	if (samplingMode == SAMPLE_LEFT)
+		x = 3;
+	else if (samplingMode == SAMPLE_RIGHT)
+		x = 50;
+	else
+		x = 104;
+
+	charOut(x, 208, 'X', video.palette[PAL_GENTXT]);
+}
+
+static void showCurrSample(void)
+{
+	updateCurrSample();
+
+	// reset sampler screen attributes
+	sampler.loopStartPos = 0;
+	sampler.loopEndPos = 0;
+	editor.markStartOfs = -1;
+	editor.markEndOfs = -1;
+	editor.samplePos = 0;
+	hideSprite(SPRITE_LOOP_PIN_LEFT);
+	hideSprite(SPRITE_LOOP_PIN_RIGHT);
+
+	renderSampleData();
+}
+
+void renderSamplingBox(void)
+{
+	editor.sampleZero = false;
+	editor.blockMarkFlag = false;
+
+	// remove all open screens (except sampler)
+	if (ui.diskOpScreenShown  || ui.posEdScreenShown || ui.editOpScreenShown)
+	{
+		ui.diskOpScreenShown = false;
+		ui.posEdScreenShown = false;
+		ui.editOpScreenShown = false;
+
+		displayMainScreen();
+	}
+	setStatusMessage("ALL RIGHT", DO_CARRY);
+
+	blit32(0, 203, 320, 52, samplingBoxBMP);
+
+	updateOutputFrequency();
+	drawSamplingNote();
+	drawSamplingFinetune();
+	drawSamplingFrequency();
+	drawSamplingModeCross();
+	renderSampleMonitor();
+
+	scanAudioDevices();
+	selectAudioDevice(selectedDev);
+
+	showCurrSample();
+
+	modStop();
+	editor.songPlaying = false;
+	editor.playMode = PLAY_MODE_NORMAL;
+	editor.currMode = MODE_IDLE;
+	pointerSetMode(POINTER_MODE_IDLE, DO_CARRY);
+}
+
+static int32_t scrPos2SmpBufPos(int32_t x) // x = 0..SAMPLE_PREVIEW_WITDH
+{
+	return (x * ((SAMPLING_BUFFER_SIZE << 16) / SAMPLE_PREVIEW_WITDH)) >> 16;
+}
+
+static uint8_t getDispBuffPeak(const int16_t *smpData, int32_t smpNum)
+{
+	int32_t smpAbs, max = 0;
+	for (int32_t i = 0; i < smpNum; i++)
+	{
+		const int32_t smp = smpData[i];
+
+		smpAbs = ABS(smp);
+		if (smpAbs > max)
+			max = smpAbs;
+	}
+
+	max = ((max * SAMPLE_PREVIEW_HEIGHT) + 32768) >> 16;
+	if (max > (SAMPLE_PREVIEW_HEIGHT/2)-1)
+		max = (SAMPLE_PREVIEW_HEIGHT/2)-1;
+
+	return (uint8_t)max;
+}
+
+void writeSampleMonitorWaveform(void) // called every frame
+{
+	if (!ui.samplingBoxShown || ui.askScreenShown)
+		return;
+
+	if (samplingEnded)
+	{
+		samplingEnded = false;
+		stopSampling();
+	}
+
+	// clear waveform background
+	fillRect(123, 58, SAMPLE_PREVIEW_WITDH, SAMPLE_PREVIEW_HEIGHT, video.palette[PAL_BACKGRD]);
+
+	if (!audioDevOpen)
+	{
+		textOutTight(136, 74, "CAN'T OPEN AUDIO DEVICE!", video.palette[PAL_QADSCP]);
+		return;
+	}
+
+	uint32_t *centerPtr = &video.frameBuffer[(76 * SCREEN_W) + 123];
+
+	// hardcoded for a buffer size of 512
+	displayingBuffer = true;
+	for (int32_t x = 0; x < SAMPLE_PREVIEW_WITDH; x++)
+	{
+		int32_t smpIdx = scrPos2SmpBufPos(x);
+		int32_t smpNum = scrPos2SmpBufPos(x+1) - smpIdx;
+
+		if (smpIdx+smpNum >= SAMPLING_BUFFER_SIZE)
+			smpNum = SAMPLING_BUFFER_SIZE - smpIdx;
+
+		const int32_t smpAbs = getDispBuffPeak(&displayBuffer[smpIdx], smpNum);
+		if (smpAbs == 0)
+			centerPtr[x] = video.palette[PAL_QADSCP];
+		else
+			vLine(x + 123, 76 - smpAbs, (smpAbs << 1) + 1, video.palette[PAL_QADSCP]);
+	}
+	displayingBuffer = false;
+}
+
+void removeSamplingBox(void)
+{
+	stopInputAudio();
+	freeAudioDeviceList();
+
+	ui.aboutScreenShown = false;
+	editor.blockMarkFlag = false;
+	displayMainScreen();
+	updateVisualizer(); // kludge
+
+	// re-render sampler screen
+	exitFromSam();
+	samplerScreen();
+}
+
+static void startSampling(void)
+{
+	if (!audioDevOpen)
+	{
+		displayErrorMsg("DEVICE ERROR !");
+		return;
+	}
+
+	assert(roundedOutputFrequency > 0);
+
+	maxSamplingLength = (int32_t)(ceil((65534.0*inputFrequency) / dOutputFrequency)) + 1;
+	
+	bufferOrig = (int16_t *)calloc(SINC_TAPS + maxSamplingLength + SINC_TAPS, sizeof (int16_t));
+	if (bufferOrig == NULL)
+	{
+		statusOutOfMemory();
+		return;
+	}
+	buffer = bufferOrig + SINC_TAPS; // allow negative look-up for sinc taps
+
+	bytesSampled = 0;
+	audio.isSampling = true;
+	samplingEnded = false;
+
+	turnOffVoices();
+
+	pointerSetMode(POINTER_MODE_RECORD, NO_CARRY);
+	setStatusMessage("SAMPLING ...", NO_CARRY);
+}
+
+static uint16_t downsampleSamplingBuffer(void)
+{
+	const int32_t readLength = bytesSampled;
+	const double dRatio = dOutputFrequency / inputFrequency;
+	
+	int32_t writeLength = (int32_t)(readLength * dRatio);
+	if (writeLength > MAX_SAMPLE_LEN)
+		writeLength = MAX_SAMPLE_LEN;
+
+	//config.normalizeSampling = false;
+
+	double *dBuffer = NULL;
+	if (config.normalizeSampling)
+	{
+		dBuffer = (double *)malloc(writeLength * sizeof (double));
+		if (dBuffer == NULL)
+		{
+			statusOutOfMemory();
+			return 0;
+		}
+	}
+
+	const double dCutoff = dRatio * 0.97; // slightly below nyquist
+	if (!initSinc(dCutoff))
+	{
+		if (config.normalizeSampling)
+			free(dBuffer);
+
+		statusOutOfMemory();
+		return 0;
+	}
+
+	changeStatusText("DOWNSAMPLING ...");
+	flipFrame();
+
+	// downsample
+
+	int8_t *output = &song->sampleData[song->samples[editor.currSample].offset];
+
+	const double dDelta = inputFrequency / dOutputFrequency;
+	int16_t *smpPtr = &buffer[-((SINC_TAPS/2)-1)]; // pre-centered (this is safe, look at how bufferOrig is alloc'd)
+
+	double dFrac = 0.0;
+	if (config.normalizeSampling)
+	{
+		double dPeakAmp = 0.0;
+		for (int32_t i = 0; i < writeLength; i++) // up to 65534 bytes
+		{
+			double dSmp = sinc(smpPtr, dFrac);
+
+			dFrac += dDelta;
+			int32_t wholeSamples = (int32_t)dFrac;
+			dFrac -= wholeSamples;
+			smpPtr += wholeSamples;
+
+			const double dAbsSmp = fabs(dSmp);
+			if (dAbsSmp > dPeakAmp)
+				dPeakAmp = dAbsSmp;
+
+			dBuffer[i] = dSmp;
+		}
+
+		// normalize
+
+		double dAmp = INT8_MAX / dPeakAmp;
+
+		/* If we have to amplify THIS much, it would mean that the gain was extremely low.
+		** We don't want to amplify a ton of noise, so keep it quantized to zero (silence).
+		*/
+		const double dAmp_dB = 20.0*log10(dAmp);
+		if (dAmp_dB > 40.0)
+			dAmp = 0.0;
+
+		for (int32_t i = 0; i < writeLength; i++)
+		{
+			/* To round the sample is probably incorrect, but it aliases audibly
+			** less after sampling a 1kHz sine wave, so I'll stick with it for now.
+			** Also just a note: Dithering is not very suitable for samples being
+			** played at lower pitches, hence why I don't dithering.
+			*/
+			const double dSmp = dBuffer[i] * dAmp;
+			int32_t smp32 = (int32_t)round(dSmp);
+			output[i] = (int8_t)CLAMP(smp32, -128, 127);
+		}
+	}
+	else
+	{
+		for (int32_t i = 0; i < writeLength; i++) // up to 65534 bytes
+		{
+			const double dSmp = sinc(smpPtr, dFrac);
+			int32_t smp32 = (int32_t)round(dSmp);
+			output[i] = (int8_t)CLAMP(smp32, -128, 127);
+
+			dFrac += dDelta;
+			int32_t wholeSamples = (int32_t)dFrac;
+			dFrac -= wholeSamples;
+			smpPtr += wholeSamples;
+		}
+	}
+	freeSinc();
+
+	if (config.normalizeSampling)
+		free(dBuffer);
+
+	return (uint16_t)writeLength;
+}
+
+void stopSampling(void)
+{
+	while (callbackBusy);
+	audio.isSampling = false;
+
+	int32_t newLength = downsampleSamplingBuffer();
+	if (newLength == 0)
+		return; // out of memory
+
+	moduleSample_t *s = &song->samples[editor.currSample];
+	s->length = (uint16_t)newLength;
+	s->fineTune = samplingFinetune;
+	s->loopStart = 0;
+	s->loopLength = 2;
+	s->volume = 64;
+	fixSampleBeep(s);
+
+	if (bufferOrig != NULL)
+	{
+		free(bufferOrig);
+		bufferOrig = NULL;
+	}
+
+	pointerSetMode(POINTER_MODE_IDLE, DO_CARRY);
+	displayMsg("SAMPLING DONE !");
+	setMsgPointer();
+
+	showCurrSample();
+}
+
+static void scrollListUp(void)
+{
+	if (numAudioInputDevs <= VISIBLE_LIST_ENTRIES)
+	{
+		audioInputDevListOffset = 0;
+		return;
+	}
+
+	if (audioInputDevListOffset > 0)
+	{
+		audioInputDevListOffset--;
+		listAudioDevices();
+		mouse.lastSamplingButton = 0;
+	}
+}
+
+static void scrollListDown(void)
+{
+	if (numAudioInputDevs <= VISIBLE_LIST_ENTRIES)
+	{
+		audioInputDevListOffset = 0;
+		return;
+	}
+
+	if (audioInputDevListOffset < numAudioInputDevs-VISIBLE_LIST_ENTRIES)
+	{
+		audioInputDevListOffset++;
+		listAudioDevices();
+		mouse.lastSamplingButton = 1;
+	}
+}
+
+static void finetuneUp(void)
+{
+	if ((int8_t)samplingFinetune < 7)
+	{
+		samplingFinetune++;
+		updateOutputFrequency();
+		drawSamplingFinetune();
+		drawSamplingFrequency();
+		mouse.lastSamplingButton = 2;
+	}
+}
+
+static void finetuneDown(void)
+{
+	if ((int8_t)samplingFinetune > -8)
+	{
+		samplingFinetune--;
+		updateOutputFrequency();
+		drawSamplingFinetune();
+		drawSamplingFrequency();
+		mouse.lastSamplingButton = 3;
+	}
+}
+
+void samplingSampleNumUp(void)
+{
+	if (editor.currSample < 30)
+	{
+		editor.currSample++;
+		showCurrSample();
+	}
+}
+
+void samplingSampleNumDown(void)
+{
+	if (editor.currSample > 0)
+	{
+		editor.currSample--;
+		showCurrSample();
+	}
+}
+
+void handleSamplingBox(void)
+{
+	if (ui.changingSamplingNote)
+	{
+		ui.changingSamplingNote = false;
+		setPrevStatusMessage();
+		pointerSetPreviousMode();
+		drawSamplingNote();
+		return;
+	}
+
+	if (mouse.rightButtonPressed)
+	{
+		if (audio.isSampling)
+			stopSampling();
+		else
+			startSampling();
+
+		return;
+	}
+
+	if (!mouse.leftButtonPressed)
+		return;
+
+	if (audio.isSampling)
+		stopSampling();
+
+	mouse.lastSamplingButton = -1;
+	mouse.repeatCounter = 0;
+
+	// check buttons
+	const int32_t mx = mouse.x;
+	const int32_t my = mouse.y;
+
+	if (mx >= 98 && mx <= 108 && my >= 44 && my <= 54) // SAMPLE UP (main UI)
+	{
+		samplingSampleNumUp();
+	}
+
+	else if (mx >= 109 && mx <= 119 && my >= 44 && my <= 54) // SAMPLE DOWN (main UI)
+	{
+		samplingSampleNumDown();
+	}
+
+	else if (mx >= 143 && mx <= 176 && my >= 205 && my <= 215) // SCAN
+	{
+		if (audio.rescanAudioDevicesSupported)
+		{
+			scanAudioDevices();
+			listAudioDevices();
+		}
+		else
+		{
+			displayErrorMsg("UNSUPPORTED !");
+		}
+	}
+
+	else if (mx >= 4 && mx <= 165 && my >= 220 && my <= 250) // DEVICE LIST
+	{
+		selectAudioDevice(audioInputDevListOffset + ((my - 220) >> 3));
+	}
+
+	else if (mx >= 2 && mx <= 41 && my >= 206 && my <= 216) // LEFT
+	{
+		if (samplingMode != SAMPLE_LEFT)
+		{
+			samplingMode = SAMPLE_LEFT;
+			drawSamplingModeCross();
+		}
+	}
+
+	else if (mx >= 49 && mx <= 95 && my >= 206 && my <= 216) // RIGHT
+	{
+		if (samplingMode != SAMPLE_RIGHT)
+		{
+			samplingMode = SAMPLE_RIGHT;
+			drawSamplingModeCross();
+		}
+	}
+
+	else if (mx >= 103 && mx <= 135 && my >= 206 && my <= 216) // MIX
+	{
+		if (samplingMode != SAMPLE_MIX)
+		{
+			samplingMode = SAMPLE_MIX;
+			drawSamplingModeCross();
+		}
+	}
+
+	else if (mx >= 188 && mx <= 237 && my >= 242 && my <= 252) // SAMPLE
+	{
+		startSampling();
+	}
+
+	else if (mx >= 242 && mx <= 277 && my >= 242 && my <= 252) // NOTE
+	{
+		ui.changingSamplingNote = true;
+		textOutBg(262, 230, "---", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
+		setStatusMessage("SELECT NOTE", NO_CARRY);
+		pointerSetMode(POINTER_MODE_MSG1, NO_CARRY);
+	}
+
+	else if (mx >= 282 && mx <= 317 && my >= 242 && my <= 252) // EXIT
+	{
+		ui.samplingBoxShown = false;
+		removeSamplingBox();
+	}
+
+	else if (mx >= 166 && mx <= 177 && my >= 218 && my <= 228) // SCROLL LIST UP
+	{
+		scrollListUp();
+	}
+
+	else if (mx >= 166 && mx <= 177 && my >= 242 && my <= 252) // SCROLL LIST DOWN
+	{
+		scrollListDown();
+	}
+
+	else if (mx >= 296 && mx <= 306 && my >= 217 && my <= 227) // FINETUNE UP
+	{
+		finetuneUp();
+	}
+
+	else if (mx >= 307 && mx <= 317 && my >= 217 && my <= 227) // FINETUNE DOWN
+	{
+		finetuneDown();
+	}
+}
+
+void setSamplingNote(uint8_t note) // must be called from video thread!
+{
+	if (note > 35)
+		note = 35;
+
+	samplingNote = note;
+	samplingFinetune = 0;
+	updateOutputFrequency();
+
+	drawSamplingNote();
+	drawSamplingFinetune();
+	drawSamplingFrequency();
+}
+
+void handleRepeatedSamplingButtons(void)
+{
+	if (!mouse.leftButtonPressed || mouse.lastSamplingButton == -1)
+		return;
+
+	switch (mouse.lastSamplingButton)
+	{
+		case 0:
+		{
+			if (mouse.repeatCounter++ >= 3)
+			{
+				mouse.repeatCounter = 0;
+				scrollListUp();
+			}
+		}
+		break;
+
+		case 1:
+		{
+			if (mouse.repeatCounter++ >= 3)
+			{
+				mouse.repeatCounter = 0;
+				scrollListDown();
+			}
+		}
+		break;
+
+		case 2:
+		{
+			if (mouse.repeatCounter++ >= 5)
+			{
+				mouse.repeatCounter = 0;
+				finetuneUp();
+			}
+		}
+		break;
+
+		case 3:
+		{
+			if (mouse.repeatCounter++ >= 5)
+			{
+				mouse.repeatCounter = 0;
+				finetuneDown();
+			}
+		}
+		break;
+
+		default: break;
+	}
+}
--- /dev/null
+++ b/src/pt2_sampling.h
@@ -1,0 +1,16 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+
+void stopSampling(void);
+void freeAudioDeviceList(void);
+void renderSampleMonitor(void);
+void setSamplingNote(uint8_t note); // must be called from video thread!
+void renderSamplingBox(void);
+void writeSampleMonitorWaveform(void);
+void removeSamplingBox(void);
+void handleSamplingBox(void);
+void handleRepeatedSamplingButtons(void);
+void samplingSampleNumUp(void);
+void samplingSampleNumDown(void);
--- a/src/pt2_scopes.c
+++ b/src/pt2_scopes.c
@@ -22,12 +22,19 @@
 // this uses code that is not entirely thread safe, but I have never had any issues so far...
 
 static volatile bool scopesUpdatingFlag, scopesDisplayingFlag;
+static int32_t oldPeriod = -1;
 static uint32_t scopeTimeLen, scopeTimeLenFrac;
 static uint64_t timeNext64, timeNext64Frac;
+static float fOldScopeDelta;
 static SDL_Thread *scopeThread;
 
 scope_t scope[AMIGA_VOICES]; // global
 
+void resetCachedScopePeriod(void)
+{
+	oldPeriod = -1;
+}
+
 int32_t getSampleReadPos(int32_t ch, uint8_t smpNum)
 {
 	const int8_t *data;
@@ -57,8 +64,64 @@
 	return pos;
 }
 
-void scopeTrigger(int32_t ch, int32_t length)
+void scopeSetVolume(int32_t ch, uint16_t vol)
 {
+	vol &= 127; // confirmed behavior on real Amiga
+
+	if (vol > 64)
+		vol = 64; // confirmed behavior on real Amiga
+
+	scope[ch].volume = (uint8_t)vol;
+}
+
+void scopeSetPeriod(int32_t ch, uint16_t period)
+{
+	int32_t realPeriod;
+
+	if (period == 0)
+		realPeriod = 1+65535; // confirmed behavior on real Amiga
+	else if (period < 113)
+		realPeriod = 113; // close to what happens on real Amiga (and needed for BLEP synthesis)
+	else
+		realPeriod = period;
+
+	// if the new period was the same as the previous period, use cached deltas
+	if (realPeriod != oldPeriod)
+	{
+		oldPeriod = realPeriod;
+
+		// this period is not cached, calculate scope delta
+
+		const float fPeriodToScopeDeltaDiv = PAULA_PAL_CLK / (float)SCOPE_HZ;
+		fOldScopeDelta = fPeriodToScopeDeltaDiv / realPeriod;
+	}
+
+	scope[ch].fDelta = fOldScopeDelta;
+}
+
+void scopeSetData(int32_t ch, const int8_t *src)
+{
+	// set voice data
+	if (src == NULL)
+		src = &song->sampleData[RESERVED_SAMPLE_OFFSET]; // dummy sample
+
+	scope[ch].newData = src;
+}
+
+void scopeSetLength(int32_t ch, uint16_t len)
+{
+	if (len == 0)
+	{
+		len = 65535;
+		/* Confirmed behavior on real Amiga (also needed for safety).
+		** And yes, we have room for this, it will never overflow!
+		*/
+	}
+	scope[ch].newLength = len << 1;
+}
+
+void scopeTrigger(int32_t ch)
+{
 	volatile scope_t *sc = &scope[ch];
 	scope_t tempState = *sc; // cache it
 
@@ -66,16 +129,14 @@
 	if (newData == NULL)
 		newData = &song->sampleData[RESERVED_SAMPLE_OFFSET]; // dummy sample
 
-	if (length < 2)
-	{
-		sc->active = false;
-		return;
-	}
+	int32_t newLength = tempState.newLength;
+	if (newLength < 2)
+		newLength = 2;
 
-	tempState.posFrac = 0;
+	tempState.fPhase = 0.0f;
 	tempState.pos = 0;
 	tempState.data = newData;
-	tempState.length = length;
+	tempState.length = newLength;
 	tempState.active = true;
 
 	/* Update live scope now.
@@ -100,14 +161,15 @@
 	for (int32_t i = 0; i < AMIGA_VOICES; i++, sc++)
 	{
 		tempState = *sc; // cache it
-
 		if (!tempState.active)
 			continue; // scope is not active
 
-		tempState.posFrac += tempState.delta;
-		tempState.pos += tempState.posFrac >> SCOPE_FRAC_BITS;
-		tempState.posFrac &= SCOPE_FRAC_MASK;
+		tempState.fPhase += tempState.fDelta;
 
+		const int32_t wholeSamples = (int32_t)tempState.fPhase;
+		tempState.fPhase -= wholeSamples;
+		tempState.pos += wholeSamples;
+
 		if (tempState.pos >= tempState.length)
 		{
 			// sample reached end, simulate Paula register update (sample swapping)
@@ -154,14 +216,10 @@
 		if (!tmpScope.active || tmpScope.data == NULL || tmpScope.volume == 0 || tmpScope.length == 0)
 			continue;
 
-		int32_t samplesToScan = tmpScope.delta >> SCOPE_FRAC_BITS; // amount of integer samples getting skipped every frame
+		int32_t samplesToScan = (int32_t)tmpScope.fDelta; // amount of integer samples getting skipped every frame
 		if (samplesToScan <= 0)
 			continue;
 
-		// shouldn't happen (low period 113 -> samplesToScan=490), but let's not waste cycles if it does
-		if (samplesToScan > 512)
-			samplesToScan = 512;
-
 		int32_t pos = tmpScope.pos;
 		int32_t length = tmpScope.length;
 		const int8_t *data = tmpScope.data;
@@ -169,14 +227,13 @@
 		int32_t runningAmplitude = 0;
 		for (int32_t x = 0; x < samplesToScan; x++)
 		{
-			int16_t amplitude = 0;
+			int32_t amplitude = 0;
 			if (data != NULL)
 				amplitude = data[pos] * tmpScope.volume;
 
 			runningAmplitude += ABS(amplitude);
 
-			pos++;
-			if (pos >= length)
+			if (++pos >= length)
 			{
 				pos = 0;
 
@@ -188,11 +245,11 @@
 			}
 		}
 
-		double dAvgAmplitude = runningAmplitude / (double)samplesToScan;
+		float fAvgAmplitude = runningAmplitude / (float)samplesToScan;
 
-		dAvgAmplitude *= (96.0 / (128.0 * 64.0)); // normalize
+		fAvgAmplitude *= 96.0f / (128.0f * 64.0f); // normalize
 
-		int32_t vuHeight = (int32_t)dAvgAmplitude;
+		int32_t vuHeight = (int32_t)(fAvgAmplitude + 0.5f); // rounded
 		if (vuHeight > 48) // max VU-meter height
 			vuHeight = 48;
 
@@ -204,14 +261,13 @@
 void drawScopes(void)
 {
 	int16_t scopeData;
-	int32_t i, x, y;
-	uint32_t *dstPtr, *scopeDrawPtr;
+	int32_t i, x;
+	uint32_t *scopeDrawPtr;
 	volatile scope_t *sc;
 	scope_t tmpScope;
 
 	scopeDrawPtr = &video.frameBuffer[(71 * SCREEN_W) + 128];
 
-	const uint32_t bgColor = video.palette[PAL_BACKGRD];
 	const uint32_t fgColor = video.palette[PAL_QADSCP];
 
 	sc = scope;
@@ -229,15 +285,8 @@
 			sc->emptyScopeDrawn = false;
 
 			// fill scope background
-			dstPtr = &video.frameBuffer[(55 * SCREEN_W) + (128 + (i * (SCOPE_WIDTH + 8)))];
-			for (y = 0; y < SCOPE_HEIGHT; y++)
-			{
-				for (x = 0; x < SCOPE_WIDTH; x++)
-					dstPtr[x] = bgColor;
+			fillRect(128 + (i * (SCOPE_WIDTH + 8)), 55, SCOPE_WIDTH, SCOPE_HEIGHT, video.palette[PAL_BACKGRD]);
 
-				dstPtr += SCREEN_W;
-			}
-
 			// render scope data
 
 			int32_t pos = tmpScope.pos;
@@ -273,14 +322,7 @@
 			if (!sc->emptyScopeDrawn)
 			{
 				// fill scope background
-				dstPtr = &video.frameBuffer[(55 * SCREEN_W) + (128 + (i * (SCOPE_WIDTH + 8)))];
-				for (y = 0; y < SCOPE_HEIGHT; y++)
-				{
-					for (x = 0; x < SCOPE_WIDTH; x++)
-						dstPtr[x] = bgColor;
-
-					dstPtr += SCREEN_W;
-				}
+				fillRect(128 + (i * (SCOPE_WIDTH + 8)), 55, SCOPE_WIDTH, SCOPE_HEIGHT, video.palette[PAL_BACKGRD]);
 
 				// draw scope line
 				for (x = 0; x < SCOPE_WIDTH; x++)
--- a/src/pt2_scopes.h
+++ b/src/pt2_scopes.h
@@ -15,13 +15,20 @@
 	bool active, emptyScopeDrawn;
 	uint8_t volume;
 	int32_t length, pos;
-	uint32_t delta, posFrac;
 
+	float fDelta, fPhase;
 	const int8_t *newData;
 	int32_t newLength;
 } scope_t;
 
-void scopeTrigger(int32_t ch, int32_t length);
+void resetCachedScopePeriod(void);
+
+void scopeSetVolume(int32_t ch, uint16_t vol);
+void scopeSetPeriod(int32_t ch, uint16_t period);
+void scopeSetData(int32_t ch, const int8_t *src);
+void scopeSetLength(int32_t ch, uint16_t len);
+void scopeTrigger(int32_t ch);
+
 int32_t getSampleReadPos(int32_t ch, uint8_t smpNum);
 void updateScopes(void);
 void drawScopes(void);
@@ -29,4 +36,4 @@
 void stopScope(int32_t ch);
 void stopAllScopes(void);
 
-extern scope_t scope[AMIGA_VOICES];
+extern scope_t scope[AMIGA_VOICES]; // pt2_scopes.c
--- /dev/null
+++ b/src/pt2_sinc.c
@@ -1,0 +1,95 @@
+/* These routines are heavily based upon code from 
+** the OpenMPT project (Tables.cpp), which has a
+** similar license.
+**
+** This code is not very readable, as I tried to
+** make it as optimized as I could. The reason I don't
+** make one big pre-calculated table is because I want
+** *many* taps while preserving fractional precision.
+**
+** There might also be some errors in how I wrote this,
+** but so far it sounds okay to my ears.
+** Let me know if I did some crucials mistakes here!
+*/
+
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <assert.h>
+#include <math.h>
+#include "pt2_sinc.h"
+
+#define CENTER_TAP ((SINC_TAPS / 2) - 1)
+
+static double *dWindowLUT, dKPi;
+
+// Compute Bessel function Izero(y) using a series approximation
+static double Izero(double y)
+{
+	double s = 1.0, ds = 1.0, d = 0.0;
+
+	do
+	{
+		d = d + 2.0;
+		ds = ds * (y * y) / (d * d);
+		s = s + ds;
+	}
+	while (ds > 1E-7 * s);
+
+	return s;
+}
+
+bool initSinc(double dCutoff) // dCutoff = 0.0 .. 0.999
+{
+	assert(SINC_TAPS > 0);
+	if (dCutoff > 0.999)
+		dCutoff = 0.999;
+
+	const double dBeta = 9.6377;
+	
+	dKPi = M_PI * dCutoff;
+
+	// generate window table
+
+	dWindowLUT = (double *)malloc(SINC_TAPS * sizeof (double));
+	if (dWindowLUT == NULL)
+		return false;
+
+	const double dMul1 = 1.0 / ((SINC_TAPS/2) * (SINC_TAPS/2));
+	const double dMul2 = (1.0 / Izero(dBeta)) * dCutoff;
+
+	double dX = CENTER_TAP;
+	for (int32_t i = 0; i < SINC_TAPS; i++)
+	{
+		dWindowLUT[i] = Izero(dBeta * sqrt(1.0 - dX * dX * dMul1)) * dMul2; // Kaiser window
+		dX -= 1.0;
+	}
+
+	return true;
+}
+
+void freeSinc(void)
+{
+	if (dWindowLUT != NULL)
+	{
+		free(dWindowLUT);
+		dWindowLUT = NULL;
+	}
+}
+
+double sinc(int16_t *smpPtr16, double dFrac)
+{
+	double dSmp = 0.0;
+	double dX = (CENTER_TAP + dFrac) * dKPi;
+
+	for (int32_t i = 0; i < SINC_TAPS; i++)
+	{
+		const double dSinc = (sin(dX) / dX) * dWindowLUT[i]; // if only I could replace this div with a mul...
+		dSmp += smpPtr16[i] * dSinc;
+		dX -= dKPi;
+	}
+
+	dSmp *= 1.0 / (SINC_TAPS / 2); // normalize (XXX: This is probably not how to do it?)
+
+	return dSmp;
+}
--- /dev/null
+++ b/src/pt2_sinc.h
@@ -1,0 +1,14 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#if defined __amd64__ || defined __i386__ || defined _WIN64 || defined _WIN32
+#define SINC_TAPS 512
+#else
+#define SINC_TAPS 128
+#endif
+
+bool initSinc(double dCutoff); // 0.0 .. 0.999
+double sinc(int16_t *smpPtr16, double dFrac);
+void freeSinc(void);
--- a/src/pt2_structs.h
+++ b/src/pt2_structs.h
@@ -75,6 +75,11 @@
 	int16_t n_period, n_note, n_wantedperiod;
 	uint16_t n_cmd, n_length, n_replen;
 	uint32_t n_scopedelta;
+
+	// for pt2_sync.c
+	uint8_t syncFlags;
+	int8_t syncAnalyzerVolume, syncVuVolume;
+	uint16_t syncAnalyzerPeriod;
 } moduleChannel_t;
 
 typedef struct module_t
@@ -112,10 +117,11 @@
 
 typedef struct mouse_t
 {
-	volatile bool setPosFlag;
+	volatile bool setPosFlag, resetCursorColorFlag;
 	bool buttonWaiting, leftButtonPressed, rightButtonPressed;
 	uint8_t repeatCounter, buttonWaitCounter;
-	int32_t x, y, lastMouseX, setPosX, setPosY, lastGUIButton, lastSmpFilterButton, prevX, prevY;
+	int32_t x, y, lastMouseX, setPosX, setPosY, lastGUIButton, prevX, prevY;
+	int32_t lastSmpFilterButton, lastSamplingButton;
 	uint32_t buttonState;
 } mouse_t;
 
@@ -203,7 +209,9 @@
 	bool leftLoopPinMoving, rightLoopPinMoving, changingSmpResample, changingDrumPadNote;
 	bool forceSampleDrag, forceSampleEdit, introScreenShown;
 	bool aboutScreenShown, clearScreenShown, posEdScreenShown, diskOpScreenShown;
-	bool samplerVolBoxShown, samplerFiltersBoxShown, editOpScreenShown;
+	bool samplerVolBoxShown, samplerFiltersBoxShown, samplingBoxShown, editOpScreenShown;
+
+	bool changingSamplingNote;
 
 	int8_t *numPtr8, tmpDisp8, pointerMode, editOpScreen, editTextType, askScreenType;
 	int8_t visualizerMode, previousPointerMode, forceVolDrag, changingChordNote;
--- /dev/null
+++ b/src/pt2_sync.c
@@ -1,0 +1,185 @@
+#include <stdint.h>
+#include <stdbool.h>
+#include "pt2_header.h"
+#include "pt2_sync.h"
+#include "pt2_scopes.h"
+#include "pt2_audio.h"
+#include "pt2_visuals.h"
+#include "pt2_tables.h"
+
+static volatile bool chQueueClearing;
+
+chSyncData_t *chSyncEntry; // globalized
+chSync_t chSync; // globalized
+
+void resetChSyncQueue(void)
+{
+	chSync.data[0].timestamp = 0;
+	chSync.writePos = 0;
+	chSync.readPos = 0;
+}
+
+static int32_t chQueueReadSize(void)
+{
+	while (chQueueClearing);
+
+	if (chSync.writePos > chSync.readPos)
+		return chSync.writePos - chSync.readPos;
+	else if (chSync.writePos < chSync.readPos)
+		return chSync.writePos - chSync.readPos + SYNC_QUEUE_LEN + 1;
+	else
+		return 0;
+}
+
+static int32_t chQueueWriteSize(void)
+{
+	int32_t size;
+
+	if (chSync.writePos > chSync.readPos)
+	{
+		size = chSync.readPos - chSync.writePos + SYNC_QUEUE_LEN;
+	}
+	else if (chSync.writePos < chSync.readPos)
+	{
+		chQueueClearing = true;
+
+		/* Buffer is full, reset the read/write pos. This is actually really nasty since
+		** read/write are two different threads, but because of timestamp validation it
+		** shouldn't be that dangerous.
+		** It will also create a small visual stutter while the buffer is getting filled,
+		** though that is barely noticable on normal buffer sizes, and it takes a minute
+		** or two at max BPM between each time (when queue size is default, 8191)
+		*/
+		chSync.data[0].timestamp = 0;
+		chSync.readPos = 0;
+		chSync.writePos = 0;
+
+		size = SYNC_QUEUE_LEN;
+
+		chQueueClearing = false;
+	}
+	else
+	{
+		size = SYNC_QUEUE_LEN;
+	}
+
+	return size;
+}
+
+bool chQueuePush(chSyncData_t t)
+{
+	if (!chQueueWriteSize())
+		return false;
+
+	assert(chSync.writePos <= SYNC_QUEUE_LEN);
+	chSync.data[chSync.writePos] = t;
+	chSync.writePos = (chSync.writePos + 1) & SYNC_QUEUE_LEN;
+
+	return true;
+}
+
+static bool chQueuePop(void)
+{
+	if (!chQueueReadSize())
+		return false;
+
+	chSync.readPos = (chSync.readPos + 1) & SYNC_QUEUE_LEN;
+	assert(chSync.readPos <= SYNC_QUEUE_LEN);
+
+	return true;
+}
+
+static chSyncData_t *chQueuePeek(void)
+{
+	if (!chQueueReadSize())
+		return NULL;
+
+	assert(chSync.readPos <= SYNC_QUEUE_LEN);
+	return &chSync.data[chSync.readPos];
+}
+
+static uint64_t getChQueueTimestamp(void)
+{
+	if (!chQueueReadSize())
+		return 0;
+
+	assert(chSync.readPos <= SYNC_QUEUE_LEN);
+	return chSync.data[chSync.readPos].timestamp;
+}
+
+void updateChannelSyncBuffer(void)
+{
+	uint8_t updateFlags[AMIGA_VOICES];
+
+	chSyncEntry = NULL;
+
+	memset(updateFlags, 0, sizeof (updateFlags)); // this is needed
+
+	const uint64_t frameTime64 = SDL_GetPerformanceCounter();
+
+	// handle channel sync queue
+
+	while (chQueueClearing);
+	while (chQueueReadSize() > 0)
+	{
+		if (frameTime64 < getChQueueTimestamp())
+			break; // we have no more stuff to render for now
+
+		chSyncEntry = chQueuePeek();
+		if (chSyncEntry == NULL)
+			break;
+
+		for (int32_t i = 0; i < AMIGA_VOICES; i++)
+			updateFlags[i] |= chSyncEntry->channels[i].flags; // yes, OR the status
+
+		if (!chQueuePop())
+			break;
+	}
+
+	/* Extra validation because of possible issues when the buffer is full
+	** and positions are being reset, which is not entirely thread safe.
+	*/
+	if (chSyncEntry != NULL && chSyncEntry->timestamp == 0)
+		chSyncEntry = NULL;
+
+	// do actual updates
+	if (chSyncEntry != NULL)
+	{
+		scope_t *s = scope;
+		syncedChannel_t *c = chSyncEntry->channels;
+		for (int32_t i = 0; i < AMIGA_VOICES; i++, s++, c++)
+		{
+			const uint8_t flags = updateFlags[i];
+			if (flags == 0)
+				continue;
+
+			if (flags & UPDATE_VOLUME)
+				scopeSetVolume(i, c->volume);
+
+			if (flags & UPDATE_PERIOD)
+				scopeSetPeriod(i, c->period);
+
+			if (flags & TRIGGER_SAMPLE)
+			{
+				s->newData = c->triggerData;
+				s->newLength = c->triggerLength << 1;
+				scopeTrigger(i);
+			}
+
+			if (flags & UPDATE_DATA)
+				scopeSetData(i, c->newData);
+
+			if (flags & UPDATE_LENGTH)
+				scopeSetLength(i, c->newLength);
+
+			if (flags & UPDATE_ANALYZER)
+				updateSpectrumAnalyzer(c->analyzerVolume, c ->analyzerPeriod);
+
+			if (flags & UPDATE_VUMETER) // for fake VU-meters only
+			{
+				if (c->vuVolume <= 64)
+					editor.vuMeterVolumes[i] = vuMeterHeights[c->vuVolume];
+			}
+		}
+	}
+}
--- /dev/null
+++ b/src/pt2_sync.h
@@ -1,0 +1,48 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+#include "pt2_header.h" // AMIGA_VOICES
+
+enum // flags
+{
+	UPDATE_VOLUME = 1,
+	UPDATE_PERIOD = 2,
+	TRIGGER_SAMPLE = 4,
+	UPDATE_DATA = 8,
+	UPDATE_LENGTH = 16,
+	UPDATE_VUMETER = 32,
+	UPDATE_ANALYZER = 64
+};
+
+// 2^n-1 - don't change this! Queue buffer is already ~1MB in size
+#define SYNC_QUEUE_LEN 8191
+
+typedef struct syncedChannel_t
+{
+	uint8_t flags;
+	const int8_t *triggerData, *newData;
+	uint16_t triggerLength, newLength;
+	uint8_t volume, vuVolume, analyzerVolume;
+	uint16_t period, analyzerPeriod;
+} syncedChannel_t;
+
+typedef struct chSyncData_t
+{
+	syncedChannel_t channels[AMIGA_VOICES];
+	uint64_t timestamp;
+} chSyncData_t;
+
+typedef struct chSync_t
+{
+	volatile int32_t readPos, writePos;
+	chSyncData_t data[SYNC_QUEUE_LEN + 1];
+} chSync_t;
+
+void resetChSyncQueue(void);
+bool chQueuePush(chSyncData_t t);
+void updateChannelSyncBuffer(void);
+
+extern chSyncData_t *chSyncEntry; // pt2_sync.c
+extern chSync_t chSync; // pt2_sync.c
+
--- a/src/pt2_tables.c
+++ b/src/pt2_tables.c
@@ -2,6 +2,27 @@
 #include <stdbool.h>
 #include "pt2_mouse.h"
 
+const char *ftuneStrTab[16] =
+{
+	" 0", "+1", "+2", "+3",
+	"+4", "+5", "+6", "+7",
+	"-8", "-7", "-6", "-5",
+	"-4", "-3", "-2", "-1"
+};
+
+const int8_t vuMeterHeights[65] =
+{
+	 0,  0,  1,  2,  2,  3,  4,  5,
+	 5,  6,  7,  8,  8,  9, 10, 11,
+	11, 12, 13, 14, 14, 15, 16, 17,
+	17, 18, 19, 20, 20, 21, 22, 23,
+	23, 24, 25, 26, 26, 27, 28, 29,
+	29, 30, 31, 32, 32, 33, 34, 35,
+	35, 36, 37, 38, 38, 39, 40, 41,
+	41, 42, 43, 44, 44, 45, 46, 47,
+	47
+};
+
 const char hexTable[16] =
 {
 	'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
@@ -423,7 +444,8 @@
 	{ 96,233,115,243, PTB_SA_RANGEBEG},
 	{116,233,135,243, PTB_SA_RANGEEND},
 	{136,233,174,243, PTB_SA_RANGECENTER},
-	{176,233,245,243, PTB_SA_RESAMPLE},
+	{176,233,210,243, PTB_SA_SAMPLE},
+	{211,233,245,243, PTB_SA_RESAMPLE},
 	{246,233,319,243, PTB_SA_RESAMPLENOTE},
 
 	{  0,244, 31,254, PTB_SA_CUT},
--- a/src/pt2_tables.h
+++ b/src/pt2_tables.h
@@ -5,6 +5,8 @@
 #include "pt2_mouse.h"
 
 // TABLES
+extern const char *ftuneStrTab[16];
+extern const int8_t vuMeterHeights[65];
 extern const char hexTable[16];
 extern const uint32_t cursorColors[6][3];
 extern const char *noteNames1[2+36];
@@ -35,7 +37,7 @@
 #define EDITOP2_BUTTONS 22
 #define EDITOP3_BUTTONS 29
 #define EDITOP4_BUTTONS 29
-#define SAMPLER_BUTTONS 24
+#define SAMPLER_BUTTONS 25
 // -----------------------------------------------
 
 extern const guiButton_t bAsk[];
--- a/src/pt2_textout.c
+++ b/src/pt2_textout.c
@@ -16,11 +16,15 @@
 
 	if (ch == '\0' || ch == ' ')
 		return;
+
+	int32_t h = FONT_CHAR_H;
+	if (ch == 5 || ch == 6) // arrow up/down has 1 more scanline
+		h++;
 	
 	srcPtr = &fontBMP[(ch & 0x7F) << 3];
 	dstPtr = &video.frameBuffer[(yPos * SCREEN_W) + xPos];
 
-	for (int32_t y = 0; y < FONT_CHAR_H; y++)
+	for (int32_t y = 0; y < h; y++)
 	{
 		for (int32_t x = 0; x < FONT_CHAR_W; x++)
 		{
@@ -33,6 +37,40 @@
 	}
 }
 
+void charOut2(uint32_t xPos, uint32_t yPos, char ch) // for static GUI text
+{
+	const uint8_t *srcPtr;
+	uint32_t *dstPtr;
+
+	if (ch == '\0' || ch == ' ')
+		return;
+
+	int32_t h = FONT_CHAR_H;
+	if (ch == 5 || ch == 6) // arrow up/down has 1 more scanline
+		h++;
+	
+	srcPtr = &fontBMP[(ch & 0x7F) << 3];
+	dstPtr = &video.frameBuffer[(yPos * SCREEN_W) + xPos];
+
+	const uint32_t fgColor = video.palette[PAL_BORDER];
+	const uint32_t bgColor = video.palette[PAL_GENBKG2];
+
+	for (int32_t y = 0; y < h; y++)
+	{
+		for (int32_t x = 0; x < FONT_CHAR_W; x++)
+		{
+			if (srcPtr[x])
+			{
+				dstPtr[x+(SCREEN_W+1)] = bgColor;
+				dstPtr[x] = fgColor;
+			}
+		}
+
+		srcPtr += 127*FONT_CHAR_W;
+		dstPtr += SCREEN_W;
+	}
+}
+
 void charOutBg(uint32_t xPos, uint32_t yPos, char ch, uint32_t fgColor, uint32_t bgColor)
 {
 	const uint8_t *srcPtr;
@@ -41,6 +79,10 @@
 	if (ch == '\0')
 		return;
 
+	int32_t h = FONT_CHAR_H;
+	if (ch == 5 || ch == 6) // arrow up/down has 1 more scanline
+		h++;
+
 	srcPtr = &fontBMP[(ch & 0x7F) << 3];
 	dstPtr = &video.frameBuffer[(yPos * SCREEN_W) + xPos];
 
@@ -47,7 +89,7 @@
 	colors[0] = bgColor;
 	colors[1] = fgColor;
 
-	for (int32_t y = 0; y < FONT_CHAR_H; y++)
+	for (int32_t y = 0; y < h; y++)
 	{
 		for (int32_t x = 0; x < FONT_CHAR_W; x++)
 			dstPtr[x] = colors[srcPtr[x]];
@@ -65,11 +107,15 @@
 	if (ch == '\0' || ch == ' ')
 		return;
 
+	int32_t h = FONT_CHAR_H;
+	if (ch == 5 || ch == 6) // arrow up/down has 1 more scanline
+		h++;
+
 	srcPtr = &fontBMP[(ch & 0x7F) << 3];
 	dstPtr1 = &video.frameBuffer[(yPos * SCREEN_W) + xPos];
 	dstPtr2 = dstPtr1 + SCREEN_W;
 
-	for (int32_t y = 0; y < FONT_CHAR_H; y++)
+	for (int32_t y = 0; y < h; y++)
 	{
 		for (int32_t x = 0; x < FONT_CHAR_W; x++)
 		{
@@ -128,6 +174,32 @@
 	}
 }
 
+void textOutN(uint32_t xPos, uint32_t yPos, const char *text, uint32_t n, uint32_t color)
+{
+	assert(text != NULL);
+
+	uint32_t x = xPos;
+	uint32_t i = 0;
+
+	while (*text != '\0' && i++ < n)
+	{
+		charOut(x, yPos, *text++, color);
+		x += FONT_CHAR_W;
+	}
+}
+
+void textOut2(uint32_t xPos, uint32_t yPos, const char *text) // for static GUI text
+{
+	assert(text != NULL);
+
+	uint32_t x = xPos;
+	while (*text != '\0')
+	{
+		charOut2(x, yPos, *text++);
+		x += FONT_CHAR_W-1;
+	}
+}
+
 void textOutTight(uint32_t xPos, uint32_t yPos, const char *text, uint32_t color)
 {
 	assert(text != NULL);
@@ -136,7 +208,21 @@
 	while (*text != '\0')
 	{
 		charOut(x, yPos, *text++, color);
-		x += (FONT_CHAR_W - 1);
+		x += FONT_CHAR_W-1;
+	}
+}
+
+void textOutTightN(uint32_t xPos, uint32_t yPos, const char *text, uint32_t n, uint32_t color)
+{
+	assert(text != NULL);
+
+	uint32_t x = xPos;
+	uint32_t i = 0;
+
+	while (*text != '\0' && i++ < n)
+	{
+		charOut(x, yPos, *text++, color);
+		x += FONT_CHAR_W-1;
 	}
 }
 
--- a/src/pt2_textout.h
+++ b/src/pt2_textout.h
@@ -3,15 +3,30 @@
 #include <stdint.h>
 #include <stdbool.h>
 
+
+#define ARROW_RIGHT 3
+#define ARROW_LEFT 4
+#define ARROW_UP 5
+#define ARROW_DOWN 6
+#define ARROW_RIGHT_STR "\x03"
+#define ARROW_LEFT_STR "\x04"
+#define ARROW_UP_STR "\x05"
+#define ARROW_DOWN_STR "\x06"
+
 void charOut(uint32_t xPos, uint32_t yPos, char ch, uint32_t color);
+void charOut2(uint32_t xPos, uint32_t yPos, char ch);
 void charOutBg(uint32_t xPos, uint32_t yPos, char ch, uint32_t fgColor, uint32_t bgColor);
 void charOutBig(uint32_t xPos, uint32_t yPos, char ch, uint32_t color);
 void charOutBigBg(uint32_t xPos, uint32_t yPos, char ch, uint32_t fgColor, uint32_t bgColor);
 void textOut(uint32_t xPos, uint32_t yPos, const char *text, uint32_t color);
+void textOut2(uint32_t xPos, uint32_t yPos, const char *text);
+void textOutN(uint32_t xPos, uint32_t yPos, const char *text, uint32_t n, uint32_t color);
 void textOutTight(uint32_t xPos, uint32_t yPos, const char *text, uint32_t color);
+void textOutTightN(uint32_t xPos, uint32_t yPos, const char *text, uint32_t n, uint32_t color);
 void textOutBg(uint32_t xPos, uint32_t yPos, const char *text, uint32_t fgColor, uint32_t bgColor);
 void textOutBig(uint32_t xPos, uint32_t yPos, const char *text, uint32_t color);
 void textOutBigBg(uint32_t xPos, uint32_t yPos, const char *text, uint32_t fgColor, uint32_t bgColor);
+
 void printOneHex(uint32_t x, uint32_t y, uint32_t value, uint32_t fontColor);
 void printTwoHex(uint32_t x, uint32_t y, uint32_t value, uint32_t fontColor);
 void printThreeHex(uint32_t x, uint32_t y, uint32_t value, uint32_t fontColor);
@@ -38,6 +53,7 @@
 void printFiveDecimalsBg(uint32_t x, uint32_t y, uint32_t value, uint32_t fontColor, uint32_t backColor);
 void printThreeDecimalsBg(uint32_t x, uint32_t y, uint32_t value, uint32_t fontColor, uint32_t backColor);
 void printTwoDecimalsBigBg(uint32_t x, uint32_t y, uint32_t value, uint32_t fontColor, uint32_t backColor);
+
 void setPrevStatusMessage(void);
 void setStatusMessage(const char *msg, bool carry);
 void displayMsg(const char *msg);
--- a/src/pt2_visuals.c
+++ b/src/pt2_visuals.c
@@ -6,6 +6,7 @@
 #include <stdio.h>
 #include <stdint.h>
 #include <stdbool.h>
+#include <string.h>
 #include <math.h> // modf()
 #ifdef _WIN32
 #define WIN32_MEAN_AND_LEAN
@@ -40,6 +41,7 @@
 #include "pt2_mod2wav.h"
 #include "pt2_config.h"
 #include "pt2_bmp.h"
+#include "pt2_sampling.h"
 
 typedef struct sprite_t
 {
@@ -63,7 +65,6 @@
 	246, 270, 278, 286, 294, 302
 };
 
-
 void updateSongInfo1(void);
 void updateSongInfo2(void);
 void updateSampler(void);
@@ -70,6 +71,105 @@
 void updatePatternData(void);
 void updateMOD2WAVDialog(void);
 
+void blit32(int32_t x, int32_t y, int32_t w, int32_t h, const uint32_t *src)
+{
+	const uint32_t *srcPtr = src;
+	uint32_t *dstPtr = &video.frameBuffer[(y * SCREEN_W) + x];
+
+	for (int32_t yy = 0; yy < h; yy++)
+	{
+		memcpy(dstPtr, srcPtr, w * sizeof (int32_t));
+
+		srcPtr += w;
+		dstPtr += SCREEN_W;
+	}
+}
+
+void putPixel(int32_t x, int32_t y, const uint32_t pixelColor)
+{
+	video.frameBuffer[(y * SCREEN_W) + x] = pixelColor;
+}
+
+void hLine(int32_t x, int32_t y, int32_t w, const uint32_t pixelColor)
+{
+	uint32_t *dstPtr = &video.frameBuffer[(y * SCREEN_W) + x];
+	for (int32_t xx = 0; xx < w; xx++)
+		dstPtr[xx] = pixelColor;
+}
+
+void vLine(int32_t x, int32_t y, int32_t h, const uint32_t pixelColor)
+{
+	uint32_t *dstPtr = &video.frameBuffer[(y * SCREEN_W) + x];
+	for (int32_t yy = 0; yy < h; yy++)
+	{
+		*dstPtr = pixelColor;
+		dstPtr += SCREEN_W;
+	}
+}
+
+void drawFramework1(int32_t x, int32_t y, int32_t w, int32_t h)
+{
+	hLine(x, y, w-1, video.palette[PAL_BORDER]);
+	vLine(x, y+1, h-2, video.palette[PAL_BORDER]);
+	hLine(x+1, y+h-1, w-1, video.palette[PAL_GENBKG2]);
+	vLine(x+w-1, y+1, h-2, video.palette[PAL_GENBKG2]);
+
+	putPixel(x, y+h-1, video.palette[PAL_GENBKG]);
+	putPixel(x+w-1, y, video.palette[PAL_GENBKG]);
+
+	fillRect(x+1, y+1, w-2, h-2, video.palette[PAL_GENBKG]);
+}
+
+void drawFramework2(int32_t x, int32_t y, int32_t w, int32_t h)
+{
+	hLine(x, y, w-1, video.palette[PAL_GENBKG2]);
+	vLine(x, y+1, h-2, video.palette[PAL_GENBKG2]);
+	hLine(x+1, y+h-1, w-1, video.palette[PAL_BORDER]);
+	vLine(x+w-1, y+1, h-2, video.palette[PAL_BORDER]);
+
+	putPixel(x, y+h-1, video.palette[PAL_GENBKG]);
+	putPixel(x+w-1, y, video.palette[PAL_GENBKG]);
+
+	fillRect(x+1, y+1, w-2, h-2, video.palette[PAL_BACKGRD]);
+}
+
+void fillRect(int32_t x, int32_t y, int32_t w, int32_t h, const uint32_t pixelColor)
+{
+	uint32_t *dstPtr = &video.frameBuffer[(y * SCREEN_W) + x];
+
+	for (int32_t yy = 0; yy < h; yy++)
+	{
+		for (int32_t xx = 0; xx < w; xx++)
+			dstPtr[xx] = pixelColor;
+
+		dstPtr += SCREEN_W;
+	}
+}
+
+void drawButton(int32_t x, int32_t y, int32_t w, const char *text)
+{
+	if (w < 2)
+		return;
+
+	const int32_t textW = (int32_t)strlen(text) * (FONT_CHAR_W-1);
+	const int32_t textX = x + (((w - 2) - textW) >> 1);
+
+	drawFramework1(x, y, w, 11);
+	textOut2(textX, y+3, text);
+}
+
+void drawUpButton(int32_t x, int32_t y)
+{
+	drawFramework1(x, y, 11, 11);
+	textOut2(x+1, y+2, ARROW_UP_STR);
+}
+
+void drawDownButton(int32_t x, int32_t y)
+{
+	drawFramework1(x, y, 11, 11);
+	textOut2(x+1, y+2, ARROW_DOWN_STR);
+}
+
 void statusAllRight(void)
 {
 	setStatusMessage("ALL RIGHT", DO_CARRY);
@@ -169,25 +269,35 @@
 	updateVisualizer();
 	handleLastGUIObjectDown();
 	drawSamplerLine();
+	writeSampleMonitorWaveform();
 }
 
-void resetAllScreens(void)
+void resetAllScreens(void) // prepare GUI for "really quit?" dialog
 {
 	editor.mixFlag = false;
 	editor.swapChannelFlag = false;
 	ui.clearScreenShown = false;
+
 	ui.changingChordNote = false;
 	ui.changingSmpResample = false;
+	ui.changingSamplingNote = false;
+	ui.changingDrumPadNote = false;
+
 	ui.pat2SmpDialogShown = false;
 	ui.disablePosEd = false;
 	ui.disableVisualizer = false;
-
+	
 	if (ui.samplerScreenShown)
 	{
+		if (ui.samplingBoxShown)
+		{
+			ui.samplingBoxShown = false;
+			removeSamplingBox();
+		}
+
 		ui.samplerVolBoxShown = false;
 		ui.samplerFiltersBoxShown = false;
-
-		displaySample();
+		displaySample(); // removes volume/filter box
 	}
 
 	if (ui.editTextFlag)
@@ -205,46 +315,19 @@
 
 void renderAskDialog(void)
 {
-	const uint32_t *srcPtr;
-	uint32_t *dstPtr;
-
 	ui.disablePosEd = true;
 	ui.disableVisualizer = true;
 
-	// render ask dialog
-
-	srcPtr = ui.pat2SmpDialogShown ? pat2SmpDialogBMP : yesNoDialogBMP;
-	dstPtr = &video.frameBuffer[(51 * SCREEN_W) + 160];
-
-	for (uint32_t y = 0; y < 39; y++)
-	{
-		memcpy(dstPtr, srcPtr, 104 * sizeof (int32_t));
-
-		srcPtr += 104;
-		dstPtr += SCREEN_W;
-	}
+	const uint32_t *srcPtr = ui.pat2SmpDialogShown ? pat2SmpDialogBMP : yesNoDialogBMP;
+	blit32(160, 51, 104, 39, srcPtr);
 }
 
 void renderBigAskDialog(void)
 {
-	const uint32_t *srcPtr;
-	uint32_t *dstPtr;
-
 	ui.disablePosEd = true;
 	ui.disableVisualizer = true;
 
-	// render custom big ask dialog
-
-	srcPtr = bigYesNoDialogBMP;
-	dstPtr = &video.frameBuffer[(44 * SCREEN_W) + 120];
-
-	for (uint32_t y = 0; y < 55; y++)
-	{
-		memcpy(dstPtr, srcPtr, 200 * sizeof (int32_t));
-
-		srcPtr += 200;
-		dstPtr += SCREEN_W;
-	}
+	blit32(120, 44, 200, 55, bigYesNoDialogBMP);
 }
 
 void showDownsampleAskDialog(void)
@@ -381,23 +464,7 @@
 		ui.updateCurrSampleFineTune = false;
 
 		if (!editor.isWAVRendering)
-		{
-			if (currSample->fineTune >= 8)
-			{
-				charOutBg(80, 36, '-', video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
-				charOutBg(88, 36, '0' + (0x10 - (currSample->fineTune & 0xF)), video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
-			}
-			else if (currSample->fineTune > 0)
-			{
-				charOutBg(80, 36, '+', video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
-				charOutBg(88, 36, '0' + (currSample->fineTune & 0xF), video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
-			}
-			else
-			{
-				charOutBg(80, 36, ' ', video.palette[PAL_GENBKG], video.palette[PAL_GENBKG]);
-				charOutBg(88, 36, '0', video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
-			}
-		}
+			textOutBg(80, 36, ftuneStrTab[currSample->fineTune & 0xF], video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
 	}
 
 	if (ui.updateCurrSampleNum)
@@ -448,7 +515,7 @@
 		ui.updateStatusText = false;
 
 		// clear background
-		textOutBg(88, 127, "                 ", video.palette[PAL_GENBKG], video.palette[PAL_GENBKG]);
+		fillRect(88, 127, 17*FONT_CHAR_W, FONT_CHAR_H, video.palette[PAL_GENBKG]);
 
 		// render status text
 		if (!editor.errorMsgActive && editor.blockMarkFlag && !ui.askScreenShown
@@ -520,15 +587,15 @@
 		charOutBg(311, 128, ' ', video.palette[PAL_GENBKG], video.palette[PAL_GENBKG]);
 		if (editor.pNoteFlag == 1)
 		{
-			video.frameBuffer[(129 * SCREEN_W) + 314] = video.palette[PAL_GENTXT];
-			video.frameBuffer[(129 * SCREEN_W) + 315] = video.palette[PAL_GENTXT];
+			putPixel(314, 129, video.palette[PAL_GENTXT]);
+			putPixel(315, 129, video.palette[PAL_GENTXT]);
 		}
 		else if (editor.pNoteFlag == 2)
 		{
-			video.frameBuffer[(128 * SCREEN_W) + 314] = video.palette[PAL_GENTXT];
-			video.frameBuffer[(128 * SCREEN_W) + 315] = video.palette[PAL_GENTXT];
-			video.frameBuffer[(130 * SCREEN_W) + 314] = video.palette[PAL_GENTXT];
-			video.frameBuffer[(130 * SCREEN_W) + 315] = video.palette[PAL_GENTXT];
+			putPixel(314, 128, video.palette[PAL_GENTXT]);
+			putPixel(315, 128, video.palette[PAL_GENTXT]);
+			putPixel(314, 130, video.palette[PAL_GENTXT]);
+			putPixel(315, 130, video.palette[PAL_GENTXT]);
 		}
 	}
 
@@ -586,9 +653,10 @@
 		ui.updateSongSize = false;
 
 		// clear background
-		textOutBg(264, 123, "      ", video.palette[PAL_GENBKG], video.palette[PAL_GENBKG]);
+		fillRect(264, 123, 6*FONT_CHAR_W, FONT_CHAR_H, video.palette[PAL_GENBKG]);
 
 		// calculate module length
+
 		uint32_t totalSampleDataSize = 0;
 		for (i = 0; i < MOD_SAMPLES; i++)
 			totalSampleDataSize += song->samples[i].length;
@@ -630,7 +698,7 @@
 	int32_t tmpSampleOffset;
 	moduleSample_t *s;
 
-	if (!ui.samplerScreenShown)
+	if (!ui.samplerScreenShown || ui.samplingBoxShown)
 		return;
 
 	assert(editor.currSample >= 0 && editor.currSample <= 30);
@@ -767,20 +835,8 @@
 
 void renderSamplerVolBox(void)
 {
-	const uint32_t *srcPtr;
-	uint32_t *dstPtr;
+	blit32(72, 154, 136, 33, samplerVolumeBMP);
 
-	srcPtr = samplerVolumeBMP;
-	dstPtr = &video.frameBuffer[(154 * SCREEN_W) + 72];
-
-	for (uint32_t y = 0; y < 33; y++)
-	{
-		memcpy(dstPtr, srcPtr, 136 * sizeof (int32_t));
-
-		srcPtr += 136;
-		dstPtr += SCREEN_W;
-	}
-
 	ui.updateVolFromText = true;
 	ui.updateVolToText = true;
 	showVolFromSlider();
@@ -798,20 +854,8 @@
 
 void renderSamplerFiltersBox(void)
 {
-	const uint32_t *srcPtr;
-	uint32_t *dstPtr;
+	blit32(65, 154, 186, 33, samplerFiltersBMP);
 
-	srcPtr = samplerFiltersBMP;
-	dstPtr = &video.frameBuffer[(154 * SCREEN_W) + 65];
-
-	for (uint32_t y = 0; y < 33; y++)
-	{
-		memcpy(dstPtr, srcPtr, 186 * sizeof (int32_t));
-
-		srcPtr += 186;
-		dstPtr += SCREEN_W;
-	}
-
 	textOut(200, 157, "HZ", video.palette[PAL_GENTXT]);
 	textOut(200, 168, "HZ", video.palette[PAL_GENTXT]);
 
@@ -829,83 +873,8 @@
 	displaySample();
 }
 
-void renderDiskOpScreen(void)
-{
-	memcpy(video.frameBuffer, diskOpScreenBMP, (99 * 320) * sizeof (int32_t));
-
-	ui.updateDiskOpPathText = true;
-	ui.updatePackText = true;
-	ui.updateSaveFormatText = true;
-	ui.updateLoadMode = true;
-	ui.updateDiskOpFileList = true;
-}
-
-void updateDiskOp(void)
-{
-	char tmpChar;
-
-	if (!ui.diskOpScreenShown || ui.posEdScreenShown)
-		return;
-
-	if (ui.updateDiskOpFileList)
-	{
-		ui.updateDiskOpFileList = false;
-		diskOpRenderFileList();
-	}
-
-	if (ui.updateLoadMode)
-	{
-		ui.updateLoadMode = false;
-
-		// draw load mode arrow
-		if (diskop.mode == 0)
-		{
-			charOutBg(147,14, ' ', video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]); // clear other box
-			charOutBg(147, 3, 0x3, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
-		}
-		else
-		{
-			charOutBg(147, 3, ' ', video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]); // clear other box
-			charOutBg(147,14, 0x3, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
-		}
-	}
-
-	if (ui.updatePackText)
-	{
-		ui.updatePackText = false;
-		textOutBg(120, 3, diskop.modPackFlg ? "ON " : "OFF", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
-	}
-
-	if (ui.updateSaveFormatText)
-	{
-		ui.updateSaveFormatText = false;
-		     if (diskop.smpSaveType == DISKOP_SMP_WAV) textOutBg(120, 14, "WAV", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
-		else if (diskop.smpSaveType == DISKOP_SMP_IFF) textOutBg(120, 14, "IFF", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
-		else if (diskop.smpSaveType == DISKOP_SMP_RAW) textOutBg(120, 14, "RAW", video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
-	}
-
-	if (ui.updateDiskOpPathText)
-	{
-		ui.updateDiskOpPathText = false;
-
-		// print disk op. path
-		for (uint32_t i = 0; i < 26; i++)
-		{
-			tmpChar = editor.currPath[ui.diskOpPathTextOffset+i];
-			if (tmpChar == '\0')
-				tmpChar = '_';
-
-			charOutBg(24 + (i * FONT_CHAR_W), 25, tmpChar, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
-		}
-	}
-}
-
 void updatePosEd(void)
 {
-	int16_t posEdPosition;
-	int32_t x, y, y2;
-	uint32_t *dstPtr, bgPixel;
-
 	if (!ui.posEdScreenShown || !ui.updatePosEd)
 		return;
 
@@ -913,14 +882,12 @@
 
 	if (!ui.disablePosEd)
 	{
-		bgPixel = video.palette[PAL_BACKGRD];
-
-		posEdPosition = song->currOrder;
+		int32_t posEdPosition = song->currOrder;
 		if (posEdPosition > song->header.numOrders-1)
 			posEdPosition = song->header.numOrders-1;
 
 		// top five
-		for (y = 0; y < 5; y++)
+		for (int32_t y = 0; y < 5; y++)
 		{
 			if (posEdPosition-(5-y) >= 0)
 			{
@@ -929,14 +896,7 @@
 			}
 			else
 			{
-				dstPtr = &video.frameBuffer[((23+(y*6)) * SCREEN_W) + 128];
-				for (y2 = 0; y2 < 5; y2++)
-				{
-					for (x = 0; x < FONT_CHAR_W*22; x++)
-						dstPtr[x] = bgPixel;
-
-					dstPtr += SCREEN_W;
-				}
+				fillRect(128, 23+(y*6), 22*FONT_CHAR_W, FONT_CHAR_H, video.palette[PAL_BACKGRD]);
 			}
 		}
 
@@ -945,7 +905,7 @@
 		printTwoDecimalsBg(160, 53, *editor.currPosEdPattDisp, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);
 
 		// bottom six
-		for (y = 0; y < 6; y++)
+		for (int32_t y = 0; y < 6; y++)
 		{
 			if (posEdPosition+y < song->header.numOrders-1)
 			{
@@ -954,14 +914,7 @@
 			}
 			else
 			{
-				dstPtr = &video.frameBuffer[((59+(y*6)) * SCREEN_W) + 128];
-				for (y2 = 0; y2 < 5; y2++)
-				{
-					for (x = 0; x < FONT_CHAR_W*22; x++)
-						dstPtr[x] = bgPixel;
-
-					dstPtr += SCREEN_W;
-				}
+				fillRect(128, 59+(y*6), 22*FONT_CHAR_W, FONT_CHAR_H, video.palette[PAL_BACKGRD]);
 			}
 		}
 
@@ -973,19 +926,7 @@
 
 void renderPosEdScreen(void)
 {
-	const uint32_t *srcPtr;
-	uint32_t *dstPtr;
-
-	srcPtr = posEdBMP;
-	dstPtr = &video.frameBuffer[120];
-
-	for (uint32_t y = 0; y < 99; y++)
-	{
-		memcpy(dstPtr, srcPtr, 200 * sizeof (int32_t));
-
-		srcPtr += 200;
-		dstPtr += SCREEN_W;
-	}
+	blit32(120, 0, 200, 99, posEdBMP);
 }
 
 void renderMuteButtons(void)
@@ -1025,22 +966,10 @@
 
 void renderClearScreen(void)
 {
-	const uint32_t *srcPtr;
-	uint32_t *dstPtr;
-
 	ui.disablePosEd = true;
 	ui.disableVisualizer = true;
 
-	srcPtr = clearDialogBMP;
-	dstPtr = &video.frameBuffer[(51 * SCREEN_W) + 160];
-
-	for (uint32_t y = 0; y < 39; y++)
-	{
-		memcpy(dstPtr, srcPtr, 104 * sizeof (int32_t));
-
-		srcPtr += 104;
-		dstPtr += SCREEN_W;
-	}
+	blit32(160, 51, 104, 39, clearDialogBMP);
 }
 
 void removeClearScreen(void)
@@ -1088,21 +1017,14 @@
 
 void removeTextEditMarker(void)
 {
-	uint32_t *dstPtr, pixel;
-
 	if (!ui.editTextFlag)
 		return;
 
-	dstPtr = &video.frameBuffer[((ui.lineCurY - 1) * SCREEN_W) + (ui.lineCurX - 4)];
-
 	if (ui.editObject == PTB_PE_PATT)
 	{
 		// position editor text editing
+		hLine(ui.lineCurX - 4, ui.lineCurY - 1, 7, video.palette[PAL_GENBKG2]);
 
-		pixel = video.palette[PAL_GENBKG2];
-		for (uint32_t x = 0; x < 7; x++)
-			dstPtr[x] = pixel;
-
 		// no need to clear the second row of pixels
 
 		ui.updatePosEd = true;
@@ -1110,35 +1032,16 @@
 	else
 	{
 		// all others
-
-		pixel = video.palette[PAL_GENBKG];
-		for (uint32_t y = 0; y < 2; y++)
-		{
-			for (uint32_t x = 0; x < 7; x++)
-				dstPtr[x] = pixel;
-
-			dstPtr += SCREEN_W;
-		}
+		fillRect(ui.lineCurX - 4, ui.lineCurY - 1, 7, 2, video.palette[PAL_GENBKG]);
 	}
 }
 
 void renderTextEditMarker(void)
 {
-	uint32_t *dstPtr, pixel;
-
 	if (!ui.editTextFlag)
 		return;
 
-	dstPtr = &video.frameBuffer[((ui.lineCurY - 1) * SCREEN_W) + (ui.lineCurX - 4)];
-	pixel = video.palette[PAL_TEXTMARK];
-
-	for (uint32_t y = 0; y < 2; y++)
-	{
-		for (uint32_t x = 0; x < 7; x++)
-			dstPtr[x] = pixel;
-
-		dstPtr += SCREEN_W;
-	}
+	fillRect(ui.lineCurX - 4, ui.lineCurY - 1, 7, 2, video.palette[PAL_TEXTMARK]);
 }
 
 static void sendMouseButtonUpEvent(uint8_t button)
@@ -1199,14 +1102,10 @@
 
 void updateVisualizer(void)
 {
-	const uint32_t *srcPtr;
-	int32_t tmpVol;
-	uint32_t *dstPtr, pixel;
-
 	if (ui.disableVisualizer || ui.diskOpScreenShown ||
 		ui.posEdScreenShown  || ui.editOpScreenShown ||
 		ui.aboutScreenShown  || ui.askScreenShown    ||
-		editor.isWAVRendering)
+		editor.isWAVRendering || ui.samplingBoxShown)
 	{
 		return;
 	}
@@ -1215,13 +1114,13 @@
 	{
 		// spectrum analyzer
 
-		dstPtr = &video.frameBuffer[(59 * SCREEN_W) + 129];
+		uint32_t *dstPtr = &video.frameBuffer[(59 * SCREEN_W) + 129];
 		for (uint32_t i = 0; i < SPECTRUM_BAR_NUM; i++)
 		{
-			srcPtr = spectrumAnaBMP;
-			pixel = video.palette[PAL_GENBKG];
+			const uint32_t *srcPtr = analyzerColorsRGB24;
+			uint32_t pixel = video.palette[PAL_GENBKG];
 
-			tmpVol = editor.spectrumVolumes[i];
+			int32_t tmpVol = editor.spectrumVolumes[i];
 			if (tmpVol > SPECTRUM_BAR_HEIGHT)
 				tmpVol = SPECTRUM_BAR_HEIGHT;
 
@@ -1267,41 +1166,19 @@
 
 void renderSpectrumAnalyzerBg(void)
 {
-	const uint32_t *srcPtr;
-	uint32_t *dstPtr;
-
-	srcPtr = spectrumVisualsBMP;
-	dstPtr = &video.frameBuffer[(44 * SCREEN_W) + 120];
-
-	for (uint32_t y = 0; y < 55; y++)
-	{
-		memcpy(dstPtr, srcPtr, 200 * sizeof (int32_t));
-
-		srcPtr += 200;
-		dstPtr += SCREEN_W;
-	}
+	blit32(120, 44, 200, 55, spectrumVisualsBMP);
 }
 
 void renderAboutScreen(void)
 {
 	char verString[16];
-	const uint32_t *srcPtr;
-	uint32_t verStringX, *dstPtr;
+	uint32_t verStringX;
 
 	if (!ui.aboutScreenShown || ui.diskOpScreenShown || ui.posEdScreenShown || ui.editOpScreenShown)
 		return;
 
-	srcPtr = aboutScreenBMP;
-	dstPtr = &video.frameBuffer[(44 * SCREEN_W) + 120];
+	blit32(120, 44, 200, 55, aboutScreenBMP);
 
-	for (uint32_t y = 0; y < 55; y++)
-	{
-		memcpy(dstPtr, srcPtr, 200 * sizeof (int32_t));
-
-		srcPtr += 200;
-		dstPtr += SCREEN_W;
-	}
-
 	// draw version string
 
 	sprintf(verString, "v%s", PROG_VER_STR);
@@ -1312,7 +1189,6 @@
 void renderEditOpMode(void)
 {
 	const uint32_t *srcPtr;
-	uint32_t *dstPtr;
 
 	// select what character box to render
 
@@ -1341,24 +1217,14 @@
 	}
 
 	// render it...
-
-	dstPtr = &video.frameBuffer[(47 * SCREEN_W) + 310];
-	for (uint32_t y = 0; y < 6; y++)
-	{
-		for (uint32_t x = 0; x < 7; x++)
-			dstPtr[x] = srcPtr[x];
-
-		srcPtr += 7;
-		dstPtr += SCREEN_W;
-	}
+	blit32(310, 47, 7, 6, srcPtr);
 }
 
 void renderEditOpScreen(void)
 {
 	const uint32_t *srcPtr;
-	uint32_t *dstPtr;
 
-	// select which background to render
+	// select which graphics to render
 	switch (ui.editOpScreen)
 	{
 		default:
@@ -1368,16 +1234,8 @@
 		case 3: srcPtr = editOpScreen4BMP; break;
 	}
 
-	// render background
-	dstPtr = &video.frameBuffer[(44 * SCREEN_W) + 120];
-	for (uint32_t y = 0; y < 55; y++)
-	{
-		memcpy(dstPtr, srcPtr, 200 * sizeof (int32_t));
+	blit32(120, 44, 200, 55, srcPtr);
 
-		srcPtr += 200;
-		dstPtr += SCREEN_W;
-	}
-
 	renderEditOpMode();
 
 	// render text and content
@@ -1420,26 +1278,11 @@
 
 void renderMOD2WAVDialog(void)
 {
-	const uint32_t *srcPtr;
-	uint32_t *dstPtr;
-
-	srcPtr = mod2wavBMP;
-	dstPtr = &video.frameBuffer[(27 * SCREEN_W) + 64];
-
-	for (uint32_t y = 0; y < 48; y++)
-	{
-		memcpy(dstPtr, srcPtr, 192 * sizeof (int32_t));
-
-		srcPtr += 192;
-		dstPtr += SCREEN_W;
-	}
+	blit32(64, 27, 192, 48, mod2wavBMP);
 }
 
 void updateMOD2WAVDialog(void)
 {
-	int32_t barLength, percent;
-	uint32_t *dstPtr, bgPixel, pixel;
-
 	if (!ui.updateMod2WavDialog)
 		return;
 
@@ -1465,44 +1308,37 @@
 			}
 
 			editor.isWAVRendering = false;
-			modSetTempo(song->currBPM); // update BPM with normal audio output rate
+			modSetTempo(song->currBPM, true); // update BPM with normal audio output rate
 			displayMainScreen();
 		}
 		else
 		{
+			if (song->rowsInTotal == 0)
+				return;
+
 			// render progress bar
 
-			percent = (uint8_t)((song->rowsCounter * 100) / song->rowsInTotal);
+			int32_t percent = (song->rowsCounter * 100) / song->rowsInTotal;
 			if (percent > 100)
 				percent = 100;
 
-			barLength = ((percent * 180) + 50) / 100;
-			dstPtr = &video.frameBuffer[(42 * SCREEN_W) + 70];
-			pixel = video.palette[PAL_GENBKG2];
-			bgPixel = video.palette[PAL_BORDER];
+			// foreground (progress)
+			const int32_t progressBarWidth = ((percent * 180) + 50) / 100;
+			if (progressBarWidth > 0)
+				fillRect(70, 42, progressBarWidth, 11, video.palette[PAL_GENBKG2]); // foreground (progress)
 
-			for (int32_t y = 0; y < 11; y++)
-			{
-				for (int32_t x = 0; x < 180; x++)
-				{
-					uint32_t color = bgPixel;
-					if (x < barLength)
-						color = pixel;
+			// background
+			int32_t bgWidth = 180 - progressBarWidth;
+			if (bgWidth > 0)
+				fillRect(70+progressBarWidth, 42, bgWidth, 11, video.palette[PAL_BORDER]);
 
-					dstPtr[x] = color;
-				}
-
-				dstPtr += SCREEN_W;
-			}
-
-			// render percentage
-			pixel = video.palette[PAL_GENTXT];
+			// draw percentage text
 			if (percent > 99)
-				printThreeDecimals(144, 45, percent, pixel);
+				printThreeDecimals(144, 45, percent, video.palette[PAL_GENTXT]);
 			else
-				printTwoDecimals(152, 45, percent, pixel);
+				printTwoDecimals(152, 45, percent, video.palette[PAL_GENTXT]);
 
-			charOut(168, 45, '%', pixel);
+			charOut(168, 45, '%', video.palette[PAL_GENTXT]);
 		}
 	}
 }
@@ -1685,18 +1521,18 @@
 	if (ui.samplerScreenShown)
 	{
 		if (!ui.diskOpScreenShown)
-			memcpy(video.frameBuffer, trackerFrameBMP, 320 * 121 * sizeof (int32_t));
+			blit32(0, 0, 320, 121, trackerFrameBMP);
 	}
 	else
 	{
 		if (!ui.diskOpScreenShown)
-			memcpy(video.frameBuffer, trackerFrameBMP, 320 * 255 * sizeof (int32_t));
+			blit32(0, 0, 320, 255, trackerFrameBMP);
 		else
-			memcpy(&video.frameBuffer[121 * SCREEN_W], &trackerFrameBMP[121 * SCREEN_W], 320 * 134 * sizeof (int32_t));
+			blit32(0, 121, 320, 134, &trackerFrameBMP[121 * SCREEN_W]);
 
 		ui.updateSongBPM = true;
 		ui.updateCurrPattText = true;
-		ui.updatePatternData  = true;
+		ui.updatePatternData = true;
 	}
 
 	if (ui.diskOpScreenShown)
@@ -1709,7 +1545,8 @@
 		ui.updateSongPattern = true;
 		ui.updateSongLength = true;
 
-		// zeroes (can't integrate zeroes in the graphics, the palette entry is above the 2-bit range)
+		// draw zeroes that will never change (to the left of numbers)
+
 		charOut(64,  3, '0', video.palette[PAL_GENTXT]);
 		textOut(64, 14, "00", video.palette[PAL_GENTXT]);
 
@@ -1982,7 +1819,7 @@
 			{
 				for (i = 0; i < 20; i++)
 				{
-					fileName[i] = (char)(tolower(song->header.name[i]));
+					fileName[i] = (char)tolower(song->header.name[i]);
 					if (fileName[i] == '\0') break;
 					sanitizeFilenameChar(&fileName[i]);
 				}
--- a/src/pt2_visuals.h
+++ b/src/pt2_visuals.h
@@ -18,6 +18,17 @@
 	SPRITE_TYPE_RGB = 1
 };
 
+void blit32(int32_t x, int32_t y, int32_t w, int32_t h, const uint32_t *src);
+void putPixel(int32_t x, int32_t y, const uint32_t pixelColor);
+void hLine(int32_t x, int32_t y, int32_t w, const uint32_t pixelColor);
+void vLine(int32_t x, int32_t y, int32_t h, const uint32_t pixelColor);
+void drawFramework1(int32_t x, int32_t y, int32_t w, int32_t h);
+void drawFramework2(int32_t x, int32_t y, int32_t w, int32_t h);
+void fillRect(int32_t x, int32_t y, int32_t w, int32_t h, const uint32_t pixelColor);
+void drawButton(int32_t x, int32_t y, int32_t w, const char *text);
+void drawUpButton(int32_t x, int32_t y);
+void drawDownButton(int32_t x, int32_t y);
+
 void statusAllRight(void);
 void statusOutOfMemory(void);
 void statusSampleIsEmpty(void);
@@ -37,7 +48,6 @@
 void updatePosEd(void);
 void updateVisualizer(void);
 void updateEditOp(void);
-void updateDiskOp(void);
 void toggleFullScreen(void);
 void videoClose(void);
 
@@ -46,7 +56,6 @@
 void renderBigAskDialog(void);
 void showDownsampleAskDialog(void);
 void renderPosEdScreen(void);
-void renderDiskOpScreen(void);
 void renderMuteButtons(void);
 void renderClearScreen(void);
 void renderAboutScreen(void);
--- a/vs2019_project/pt2-clone/protracker.ini
+++ b/vs2019_project/pt2-clone/protracker.ini
@@ -79,8 +79,6 @@
 ;
 HWMOUSE=TRUE
 
-
-
 [GENERAL SETTINGS]
 ; Hide last modification dates in Disk Op. to get longer dir/file names
 ;        Syntax: TRUE or FALSE
@@ -198,12 +196,10 @@
 ;
 MODDOT=FALSE
 
-; Dotted line in center of sample data view
+; Draw a center line in the SAMPLER screen's waveform
 ;        Syntax: TRUE or FALSE
 ; Default value: TRUE
-;       Comment: Setting it to FALSE will turn off the dotted center line
-;         that is rendered in the middle of the sampler data view in
-;         the sampler screen.
+;       Comment: This used to draw a dotted line, but now draws a line instead
 ;
 DOTTEDCENTER=TRUE
 
@@ -211,20 +207,37 @@
 ; Audio output frequency
 ;        Syntax: Number, in hertz
 ; Default value: 48000
-;       Comment: Ranges from 44100 to 192000.
-;         Also sets the playback frequency for WAVs made with MOD2WAV.
+;       Comment: Ranges from 44100 to 192000. 96000 is recommended if your
+;         OS is set to mix shared audio at 96kHz or higher.
 ;
 FREQUENCY=48000
 
+; Audio input frequency
+;        Syntax: Number, in hertz
+; Default value: 44100
+;       Comment: Ranges from 44100 to 192000. This should be set to match
+;         the frequency used for your audio input device (for sampling).
+;
+SAMPLINGFREQ=44100
+
+; Normalize sampled audio before converting to 8-bit
+;        Syntax: TRUE or FALSE
+; Default value: TRUE
+;       Comment: This one is for the audio sampling feature in the SAMPLER
+;         screen. If it's set to TRUE, it will normalize the gain before it
+;         converts the sample to 8-bit in the end. This will preserve as much
+;         amplitude information as possible to lower quantization noise.
+;
+NORMALIZESAMPLING=TRUE
+
 ; Audio buffer size
 ;        Syntax: Number, in samples
 ; Default value: 1024
 ;       Comment: Ranges from 128 to 8192. Should be a number that is 2^n
-;          (128, 256, 512, 1024, 2048, 4096, 8192). The number you input isn't
-;          necessarily the final value the audio API decides to use.
+;          (128, 256, 512, 1024, 2048, 4096, 8192, ...). The number you input
+;          isn't necessarily the actual value the audio API decides to use.
 ;          Lower means less audio latency but possible audio issues, higher
-;          means more audio latency but less chance for issues. This will also
-;          change the latency of the VU-meters, spectrum analyzer and scopes.
+;          means more audio latency but less chance for issues.
 ;
 BUFFERSIZE=1024
 
@@ -231,15 +244,12 @@
 ; Amiga 500 low-pass filter (not the "LED" filter)
 ;        Syntax: TRUE or FALSE
 ; Default value: FALSE
-;       Comment: Use a low-pass filter to prevent some
-;         of the aliasing in the sound at the expense of
-;         sound sharpness.
-;         Every Amiga had a low-pass filter like this. All of them except
-;         for Amiga 1200 (~28..31kHz) had it set to something around
-;         4kHz to 5kHz (~4.4kHz).
-;         This must not be confused with the LED filter which can be turned
-;         on/off in software-- the low-pass filter is always enabled and
-;         can't be turned off.
+;       Comment: Enabling this will simulate the ~4421Hz 6dB/oct RC low-pass
+;         filter present in almost all Amigas. This will make the sound a bit
+;         muddier. On Amiga 1200, the cut-off is ~34kHz (sharper sound). This
+;         can also be toggled in the tracker by pressing F12. This must not be
+;         confused with the "LED" filter which can be toggled with the pattern
+;         command E0x.
 ;
 A500LOWPASSFILTER=FALSE
 
--- a/vs2019_project/pt2-clone/pt2-clone.vcxproj
+++ b/vs2019_project/pt2-clone/pt2-clone.vcxproj
@@ -290,8 +290,11 @@
     <ClInclude Include="..\..\src\pt2_sample_loader.h" />
     <ClInclude Include="..\..\src\pt2_sampler.h" />
     <ClInclude Include="..\..\src\pt2_sample_saver.h" />
+    <ClInclude Include="..\..\src\pt2_sampling.h" />
     <ClInclude Include="..\..\src\pt2_scopes.h" />
+    <ClInclude Include="..\..\src\pt2_sinc.h" />
     <ClInclude Include="..\..\src\pt2_structs.h" />
+    <ClInclude Include="..\..\src\pt2_sync.h" />
     <ClInclude Include="..\..\src\pt2_tables.h" />
     <ClInclude Include="..\..\src\pt2_textout.h" />
     <ClInclude Include="..\..\src\pt2_unicode.h" />
@@ -338,8 +341,11 @@
     <ClCompile Include="..\..\src\pt2_sample_loader.c" />
     <ClCompile Include="..\..\src\pt2_sampler.c" />
     <ClCompile Include="..\..\src\pt2_sample_saver.c" />
+    <ClCompile Include="..\..\src\pt2_sampling.c" />
     <ClCompile Include="..\..\src\pt2_scopes.c" />
+    <ClCompile Include="..\..\src\pt2_sinc.c" />
     <ClCompile Include="..\..\src\pt2_structs.c" />
+    <ClCompile Include="..\..\src\pt2_sync.c" />
     <ClCompile Include="..\..\src\pt2_tables.c" />
     <ClCompile Include="..\..\src\pt2_textout.c" />
     <ClCompile Include="..\..\src\pt2_unicode.c" />
--- a/vs2019_project/pt2-clone/pt2-clone.vcxproj.filters
+++ b/vs2019_project/pt2-clone/pt2-clone.vcxproj.filters
@@ -84,6 +84,15 @@
     <ClInclude Include="..\..\src\pt2_module_saver.h">
       <Filter>headers</Filter>
     </ClInclude>
+    <ClInclude Include="..\..\src\pt2_sampling.h">
+      <Filter>headers</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\src\pt2_sync.h">
+      <Filter>headers</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\src\pt2_sinc.h">
+      <Filter>headers</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <ClCompile Include="..\..\src\pt2_audio.c" />
@@ -166,6 +175,9 @@
     <ClCompile Include="..\..\src\pt2_pattern_viewer.c" />
     <ClCompile Include="..\..\src\pt2_module_saver.c" />
     <ClCompile Include="..\..\src\pt2_replayer.c" />
+    <ClCompile Include="..\..\src\pt2_sampling.c" />
+    <ClCompile Include="..\..\src\pt2_sync.c" />
+    <ClCompile Include="..\..\src\pt2_sinc.c" />
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="..\..\src\pt2-clone.rc" />