diff --git a/firmware/application/CMakeLists.txt b/firmware/application/CMakeLists.txt
index 0025e270ffe69268438c2bdfdd962ee11e181cdf..876caee76135b63d3ca8d126d667187fa0283508 100644
--- a/firmware/application/CMakeLists.txt
+++ b/firmware/application/CMakeLists.txt
@@ -143,7 +143,7 @@ set(CPPSRC
 	${COMMON}/portapack_persistent_memory.cpp
 	${COMMON}/portapack_shared_memory.cpp
 	${COMMON}/sonde_packet.cpp
-	${COMMON}/test_packet.cpp
+	# ${COMMON}/test_packet.cpp
 	${COMMON}/tpms_packet.cpp
 	${COMMON}/ui.cpp
 	${COMMON}/ui_focus.cpp
@@ -239,7 +239,7 @@ set(CPPSRC
 	apps/ui_sonde.cpp
 	apps/ui_soundboard.cpp
 	apps/ui_sstvtx.cpp
-	apps/ui_test.cpp
+	# apps/ui_test.cpp
 	apps/ui_tone_search.cpp
 	apps/ui_touch_calibration.cpp
 	apps/ui_touchtunes.cpp
diff --git a/firmware/application/apps/analog_audio_app.cpp b/firmware/application/apps/analog_audio_app.cpp
index 448ed602a25aa4bbf339fa7747faed64ebae5178..02b52f0d7a35d5ceaa6293636a2f99c4501ffddf 100644
--- a/firmware/application/apps/analog_audio_app.cpp
+++ b/firmware/application/apps/analog_audio_app.cpp
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
+ * Copyright (C) 2018 Furrtek
  *
  * This file is part of PortaPack.
  *
@@ -100,10 +101,8 @@ AnalogAudioView::AnalogAudioView(
 		&field_volume,
 		&text_ctcss,
 		&record_view,
-		&waterfall,
+		&waterfall
 	});
-	
-	//exit_on_squelch = eos;
 
 	field_frequency.set_value(receiver_model.tuning_frequency());
 	field_frequency.set_step(receiver_model.frequency_step());
@@ -177,7 +176,7 @@ void AnalogAudioView::on_hide() {
 
 void AnalogAudioView::set_parent_rect(const Rect new_parent_rect) {
 	View::set_parent_rect(new_parent_rect);
-
+	
 	const ui::Rect waterfall_rect { 0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height };
 	waterfall.set_parent_rect(waterfall_rect);
 }
@@ -208,9 +207,9 @@ void AnalogAudioView::remove_options_widget() {
 		remove_child(options_widget.get());
 		options_widget.reset();
 	}
-
+	
 	text_ctcss.hidden(true);
-
+	
 	field_lna.set_style(nullptr);
 	options_modulation.set_style(nullptr);
 	field_frequency.set_style(nullptr);
@@ -258,12 +257,23 @@ void AnalogAudioView::on_show_options_modulation() {
 	switch(modulation) {
 	case ReceiverModel::Mode::AMAudio:
 		widget = std::make_unique<AMOptionsView>(options_view_rect, &style_options_group);
+		waterfall.set_fft_widget(false);
 		break;
 
 	case ReceiverModel::Mode::NarrowbandFMAudio:
 		widget = std::make_unique<NBFMOptionsView>(nbfm_view_rect, &style_options_group);
+		waterfall.set_fft_widget(false);
 		break;
-
+	
+	case ReceiverModel::Mode::WidebandFMAudio:
+		waterfall.set_fft_widget(true);
+		waterfall.on_show();
+		break;
+	
+	case ReceiverModel::Mode::SpectrumAnalysis:
+		waterfall.set_fft_widget(false);
+		break;
+		
 	default:
 		break;
 	}
@@ -341,6 +351,7 @@ void AnalogAudioView::handle_coded_squelch(const uint32_t value) {
 	size_t min_idx { 0 };
 	size_t c;
 	
+	// Find nearest match
 	for (c = 0; c < tone_keys.size(); c++) {
 		diff = abs(((float)value / 100.0) - tone_keys[c].second);
 		if (diff < min_diff) {
@@ -349,6 +360,7 @@ void AnalogAudioView::handle_coded_squelch(const uint32_t value) {
 		}
 	}
 	
+	// Arbitrary confidence threshold
 	if (min_diff < 40)
 		text_ctcss.set("CTCSS " + tone_keys[min_idx].first);
 	else
diff --git a/firmware/application/apps/analog_audio_app.hpp b/firmware/application/apps/analog_audio_app.hpp
index 58c1fd5037260a1cdb7acdeb0fc9c200ccd59aa4..1375c24cc64de9d837416ef2dce8e6526604c763 100644
--- a/firmware/application/apps/analog_audio_app.hpp
+++ b/firmware/application/apps/analog_audio_app.hpp
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
+ * Copyright (C) 2018 Furrtek
  *
  * This file is part of PortaPack.
  *
diff --git a/firmware/application/main.cpp b/firmware/application/main.cpp
index ec7a4a842dc754ec4bea2f6aacf2e8b56ce92c8d..6a29e0add398ab97f63060548303cc4edbd7c8b3 100755
--- a/firmware/application/main.cpp
+++ b/firmware/application/main.cpp
@@ -27,6 +27,10 @@
 // Check what ends up in the BSS section by looking at the map files !
 // Use constexpr where possible or make sure const are in .cpp files, not headers !
 
+// Note about messages:
+// There can only be one message handler for one kind of message at once
+// If an attempt is made to register a second handler, there's a chDbgPanic
+
 //TEST: Goertzel tone detect
 //TEST: Menuview refresh, seems to blink a lot
 //TEST: Check AFSK transmit end, skips last bits ?
@@ -36,7 +40,11 @@
 //BUG: SCANNER Lock on frequency, if frequency jump, still locked on first one
 //BUG: SCANNER Multiple slices
 //GLITCH: The about view scroller sometimes misses lines because of a race condition between the display scrolling and drawing the line
+//GLITCH: Start of tx using ReplayThread plays a small bit of previous transmission (content of 1 buffer ?)
+//	See fifo.reset_in() ?
 
+//TODO: DCS decoder
+//TODO: Make CTCSS display only when squelch is opened
 //TODO: Make play button larger in Replay
 //TODO: Put LNA and VGA controls in Soundboard
 //TODO: Add default headphones volume setting in Audio settings
diff --git a/firmware/application/ui/ui_spectrum.cpp b/firmware/application/ui/ui_spectrum.cpp
index c6b9b5c35d2273b3d99f1463bd9efac8beee0a48..912ce10c7fa744953ed0df83f7e3dc234bcddceb 100644
--- a/firmware/application/ui/ui_spectrum.cpp
+++ b/firmware/application/ui/ui_spectrum.cpp
@@ -255,7 +255,7 @@ WaterfallWidget::WaterfallWidget(const bool cursor) {
 	
 	add_children({
 		&waterfall_view,
-		&frequency_scale,
+		&frequency_scale
 	});
 }
 
@@ -311,16 +311,51 @@ bool WaterfallWidget::on_key(const KeyEvent key) {
 	return false;
 }
 
+void WaterfallWidget::on_audio_spectrum(const AudioSpectrum& spectrum) {
+	if (fft_widget) {
+		for (size_t i = 0; i < spectrum.db.size(); i++)
+			audio_spectrum[i] = ((int16_t)spectrum.db[i] - 127) * 256;
+		fft_widget->set_dirty();
+	}
+}
+
+void WaterfallWidget::set_fft_widget(const bool show) {
+	if (fft_widget) {
+		audio_fifo = nullptr;
+		remove_child(fft_widget.get());
+		fft_widget.reset();
+	}
+	
+	if (show) {
+		waterfall_view.set_parent_rect(waterfall_reduced_rect);
+		waterfall_view.on_show();
+		fft_widget = std::make_unique<Waveform>(
+			fft_widget_rect,
+			audio_spectrum,
+			128,
+			0,
+			false,
+			Color::white());
+		add_child(fft_widget.get());
+	} else {
+		waterfall_view.set_parent_rect(waterfall_normal_rect);
+		waterfall_view.on_show();
+	}
+}
+
 void WaterfallWidget::set_parent_rect(const Rect new_parent_rect) {
 	constexpr Dim scale_height = 20;
 
 	View::set_parent_rect(new_parent_rect);
+	
+	waterfall_normal_rect = { 0, scale_height, new_parent_rect.width(), new_parent_rect.height() - scale_height};
+	waterfall_reduced_rect = { 0, scale_height, new_parent_rect.width(), new_parent_rect.height() - scale_height - audio_spectrum_height };
+	
 	frequency_scale.set_parent_rect({ 0, 0, new_parent_rect.width(), scale_height });
-	waterfall_view.set_parent_rect({
-		0, scale_height,
-		new_parent_rect.width(),
-		new_parent_rect.height() - scale_height
-	});
+	waterfall_view.set_parent_rect(waterfall_normal_rect);
+	waterfall_view.on_show();
+	
+	fft_widget_rect = { 0, new_parent_rect.height() - audio_spectrum_height, new_parent_rect.width(), audio_spectrum_height };
 }
 
 void WaterfallWidget::paint(Painter& painter) {
diff --git a/firmware/application/ui/ui_spectrum.hpp b/firmware/application/ui/ui_spectrum.hpp
index 0a207f52e65084d4097a2c91edec5fc0954af96a..425a347cb12623d873576ddbc3f3c2176ece4e20 100644
--- a/firmware/application/ui/ui_spectrum.hpp
+++ b/firmware/application/ui/ui_spectrum.hpp
@@ -97,41 +97,67 @@ public:
 	bool on_key(const KeyEvent key) override;
 
 	void set_parent_rect(const Rect new_parent_rect) override;
+	
+	void set_fft_widget(const bool show);
 
 	void paint(Painter& painter) override;
 
 private:
 	void on_tick_second();
 	
+	static constexpr ui::Dim audio_spectrum_height = 2 * 16;
+	
 	WaterfallView waterfall_view { };
 	FrequencyScale frequency_scale { };
-	ChannelSpectrumFIFO* fifo { nullptr };
 	
+	ChannelSpectrumFIFO* channel_fifo { nullptr };
+	AudioSpectrumFIFO* audio_fifo { nullptr };
+	
+	std::unique_ptr<Widget> fft_widget { };
 	bool _blink { false };
 	int sampling_rate { 0 };
 	int32_t cursor_position { 0 };
 	SignalToken signal_token_tick_second { };
+	int16_t audio_spectrum[128] { 0 };
+	ui::Rect waterfall_normal_rect { };
+	ui::Rect waterfall_reduced_rect { };
+	ui::Rect fft_widget_rect { };
 
-	MessageHandlerRegistration message_handler_spectrum_config {
+	MessageHandlerRegistration message_handler_channel_spectrum_config {
 		Message::ID::ChannelSpectrumConfig,
 		[this](const Message* const p) {
 			const auto message = *reinterpret_cast<const ChannelSpectrumConfigMessage*>(p);
-			this->fifo = message.fifo;
+			this->channel_fifo = message.fifo;
+		}
+	};
+	MessageHandlerRegistration message_handler_audio_spectrum_config {
+		Message::ID::AudioSpectrumConfig,
+		[this](const Message* const p) {
+			const auto message = *reinterpret_cast<const AudioSpectrumConfigMessage*>(p);
+			this->audio_fifo = message.fifo;
 		}
 	};
 	MessageHandlerRegistration message_handler_frame_sync {
 		Message::ID::DisplayFrameSync,
 		[this](const Message* const) {
-			if( this->fifo ) {
+			if( this->channel_fifo ) {
 				ChannelSpectrum channel_spectrum;
-				while( fifo->out(channel_spectrum) ) {
+				while( channel_fifo->out(channel_spectrum) ) {
 					this->on_channel_spectrum(channel_spectrum);
 				}
 			}
+			if( this->audio_fifo ) {
+				AudioSpectrum audio_spectrum;
+				while( audio_fifo->out(audio_spectrum) ) {
+					// Unstack everything until and only use last buffer (should only be one max. ready per frame)
+				}
+				this->on_audio_spectrum(audio_spectrum);
+			}
 		}
 	};
 
 	void on_channel_spectrum(const ChannelSpectrum& spectrum);
+	void on_audio_spectrum(const AudioSpectrum& spectrum);
 };
 
 } /* namespace spectrum */
diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp
index ff4615fe39871127b3cf1692a0a8e49be3391b20..0f4a0de548d203a94907ba96b9e6cc287eccf062 100644
--- a/firmware/application/ui_navigation.cpp
+++ b/firmware/application/ui_navigation.cpp
@@ -60,7 +60,7 @@
 #include "ui_sonde.hpp"
 #include "ui_soundboard.hpp"
 #include "ui_sstvtx.hpp"
-#include "ui_test.hpp"
+//#include "ui_test.hpp"
 #include "ui_tone_search.hpp"
 #include "ui_touchtunes.hpp"
 #include "ui_view_wav.hpp"
diff --git a/firmware/baseband/proc_wfm_audio.cpp b/firmware/baseband/proc_wfm_audio.cpp
index 89abe9d35c99576b110b15bc24cd44a23c95d1d7..27419a9f8e91a247be67c1245b4d211737e318e1 100644
--- a/firmware/baseband/proc_wfm_audio.cpp
+++ b/firmware/baseband/proc_wfm_audio.cpp
@@ -22,8 +22,9 @@
 
 #include "proc_wfm_audio.hpp"
 
+#include "portapack_shared_memory.hpp"
 #include "audio_output.hpp"
-
+#include "dsp_fft.hpp"
 #include "event_m4.hpp"
 
 #include <cstdint>
@@ -63,7 +64,50 @@ void WidebandFMAudio::execute(const buffer_c8_t& buffer) {
 	 * -> 4th order CIC decimation by 2, gain of 1
 	 * -> 96kHz int16_t[64] */
 	auto audio_2fs = audio_dec_2.execute(audio_4fs, work_audio_buffer);
-
+	
+	// Input: 96kHz int16_t[64]
+	// audio_spectrum_decimator piles up 256 bytes before doing FFT computation
+	// This should send an AudioSpectrum every sample rate/buffer size/(256/64)/refresh scaler = 3072000/2048/4/8 = ~47 Hz
+	// 0~3: feed continuous audio
+	// 4~31: ignore, wrap at 31
+	if (!(refresh_timer & 0xF8)) {
+		for (size_t i = 0; i < 64; i++) {
+			complex_audio[i] = { (int16_t)(work_audio_buffer.p[i] / 32), (int16_t)0 };
+		}
+		audio_spectrum_decimator.feed(
+			complex_audio_buffer,
+			[this](const buffer_c16_t& data) {
+				this->post_message(data);
+			}
+		);
+	} else {
+		// Spread the FFT workload in time to avoid making the audio skip
+		// "8" comes from the log2() of the size of audio_spectrum: log2(256) = 8
+		if (fft_stage && (fft_stage <= 8)) {
+			fft_c_preswapped(audio_spectrum, fft_stage - 1, fft_stage);
+			fft_stage++;
+		} else if (fft_stage > 8) {
+			AudioSpectrum spectrum;
+			const size_t spectrum_end = spectrum.db.size();
+			for(size_t i=0; i<spectrum_end; i++) {
+				//const auto corrected_sample = spectrum_window_hamming_3(audio_spectrum, i);
+				const auto corrected_sample = audio_spectrum[i];
+				const auto mag2 = magnitude_squared(corrected_sample * (1.0f / 32768.0f));
+				const float db = mag2_to_dbv_norm(mag2);
+				constexpr float mag_scale = 5.0f;
+				const unsigned int v = (db * mag_scale) + 255.0f;
+				spectrum.db[i] = std::max(0U, std::min(255U, v));
+			}
+			fifo.in(spectrum);
+			fft_stage = 0;
+		}
+	}
+	
+	if (refresh_timer == 31)
+		refresh_timer = 0;
+	else
+		refresh_timer++;
+	
 	/* 96kHz int16_t[64]
 	 * -> FIR filter, <15kHz (0.156fs) pass, >19kHz (0.198fs) stop, gain of 1
 	 * -> 48kHz int16_t[32] */
@@ -74,6 +118,12 @@ void WidebandFMAudio::execute(const buffer_c8_t& buffer) {
 	
 }
 
+void WidebandFMAudio::post_message(const buffer_c16_t& data) {
+	// This is called when audio_spectrum_decimator is filled up to 256 samples
+	fft_swap(data, audio_spectrum);
+	fft_stage = 1;
+}
+
 void WidebandFMAudio::on_message(const Message* const message) {
 	switch(message->id) {
 	case Message::ID::UpdateSpectrum:
@@ -117,6 +167,9 @@ void WidebandFMAudio::configure(const WFMConfigureMessage& message) {
 	channel_spectrum.set_decimation_factor(1);
 
 	configured = true;
+	
+	AudioSpectrumConfigMessage config_message { &fifo };
+	shared_memory.application_queue.push(config_message);
 }
 
 void WidebandFMAudio::capture_config(const CaptureConfigMessage& message) {
diff --git a/firmware/baseband/proc_wfm_audio.hpp b/firmware/baseband/proc_wfm_audio.hpp
index 669410c6bf324325b708152000f5c7ccbd744661..b84bdbddd03c5d989daf74aac1c2095289cfdd5a 100644
--- a/firmware/baseband/proc_wfm_audio.hpp
+++ b/firmware/baseband/proc_wfm_audio.hpp
@@ -27,8 +27,10 @@
 #include "baseband_thread.hpp"
 #include "rssi_thread.hpp"
 
+#include "dsp_types.hpp"
 #include "dsp_decimate.hpp"
 #include "dsp_demodulate.hpp"
+#include "block_decimator.hpp"
 
 #include "audio_output.hpp"
 #include "spectrum_collector.hpp"
@@ -56,6 +58,12 @@ private:
 		sizeof(dst) / sizeof(int16_t)
 	};
 
+	std::array<complex16_t, 64> complex_audio { };
+	const buffer_c16_t complex_audio_buffer {
+		complex_audio.data(),
+		complex_audio.size()
+	};
+	
 	dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0 { };
 	dsp::decimate::FIRC16xR16x16Decim2 decim_1 { };
 	uint32_t channel_filter_pass_f = 0;
@@ -67,6 +75,14 @@ private:
 	dsp::decimate::FIR64AndDecimateBy2Real audio_filter { };
 
 	AudioOutput audio_output { };
+	
+	// For fs=96kHz FFT streaming
+	BlockDecimator<complex16_t, 256> audio_spectrum_decimator { 1 };
+	AudioSpectrum fifo_data[1 << AudioSpectrumConfigMessage::fifo_k] { };
+	AudioSpectrumFIFO fifo { fifo_data, AudioSpectrumConfigMessage::fifo_k };
+	std::array<std::complex<float>, 256> audio_spectrum { };
+	uint32_t refresh_timer { 0 };
+	uint32_t fft_stage { 0 };
 
 	SpectrumCollector channel_spectrum { };
 	size_t spectrum_interval_samples = 0;
@@ -75,6 +91,7 @@ private:
 	bool configured { false };
 	void configure(const WFMConfigureMessage& message);
 	void capture_config(const CaptureConfigMessage& message);
+	void post_message(const buffer_c16_t& data);
 };
 
 #endif/*__PROC_WFM_AUDIO_H__*/
diff --git a/firmware/baseband/spectrum_collector.cpp b/firmware/baseband/spectrum_collector.cpp
index ebe55a24d269937402b67b8ba5e26ff382e0750d..b43daadb5d4d3b0a77321449522142218e7436a1 100644
--- a/firmware/baseband/spectrum_collector.cpp
+++ b/firmware/baseband/spectrum_collector.cpp
@@ -132,7 +132,7 @@ void SpectrumCollector::update() {
 	// Called from idle thread (after EVT_MASK_SPECTRUM is flagged)
 	if( streaming && channel_spectrum_request_update ) {
 		/* Decimated buffer is full. Compute spectrum. */
-		fft_c_preswapped(channel_spectrum);
+		fft_c_preswapped(channel_spectrum, 0, 8);
 
 		ChannelSpectrum spectrum;
 		spectrum.sampling_rate = channel_spectrum_sampling_rate;
diff --git a/firmware/common/dsp_fft.hpp b/firmware/common/dsp_fft.hpp
index 65dd80987f337aeb74f6586ac5a61f64a6484acf..c3884689b27cb1fbfd84a3fac3ca887b8fa28550 100644
--- a/firmware/common/dsp_fft.hpp
+++ b/firmware/common/dsp_fft.hpp
@@ -100,25 +100,26 @@ void fft_swap_in_place(std::array<T, N>& data) {
 /* http://www.drdobbs.com/cpp/a-simple-and-efficient-fft-implementatio/199500857?pgno=3 */
 
 template<typename T, size_t N>
-void fft_c_preswapped(std::array<T, N>& data) {
+void fft_c_preswapped(std::array<T, N>& data, const size_t from, const size_t to) {
 	static_assert(power_of_two(N), "only defined for N == power of two");
 	constexpr auto K = log_2(N);
+	if ((to > K) || (from > K)) return;
 
 	constexpr size_t K_max = 8;
 	static_assert(K <= K_max, "No FFT twiddle factors for K > 8");
 	static constexpr std::array<std::complex<float>, K_max> wp_table { {
-		{ -2.0f,                        0.0f                     },
-		{ -1.0f,                       -1.0f                     },
-		{ -0.2928932188134524756f,     -0.7071067811865475244f   },
-		{ -0.076120467488713243872f,   -0.38268343236508977173f  },
-		{ -0.019214719596769550874f,   -0.19509032201612826785f  },
-		{ -0.0048152733278031137552f,  -0.098017140329560601994f },
-		{ -0.0012045437948276072852f,  -0.049067674327418014255f },
-		{ -0.00030118130379577988423f, -0.024541228522912288032f },
+		{ -2.0f,                        0.0f                     },	// 2
+		{ -1.0f,                       -1.0f                     },	// 4
+		{ -0.2928932188134524756f,     -0.7071067811865475244f   },	// 8
+		{ -0.076120467488713243872f,   -0.38268343236508977173f  },	// 16
+		{ -0.019214719596769550874f,   -0.19509032201612826785f  },	// 32
+		{ -0.0048152733278031137552f,  -0.098017140329560601994f },	// 64
+		{ -0.0012045437948276072852f,  -0.049067674327418014255f },	// 128
+		{ -0.00030118130379577988423f, -0.024541228522912288032f },	// 256
 	} };
 
 	/* Provide data to this function, pre-swapped. */
-	for(size_t k = 0; k < log_2(N); k++) {
+	for(size_t k = from; k < to; k++) {
 		const size_t mmax = 1 << k;
 		const auto wp = wp_table[k];
 		T w { 1.0f, 0.0f };
diff --git a/firmware/common/lcd_ili9341.cpp b/firmware/common/lcd_ili9341.cpp
index c4f2fcbba69316dc432cea8efe7104dedb486f71..db7a808b798c36fcbf0e566e4731cd2aa548f94a 100644
--- a/firmware/common/lcd_ili9341.cpp
+++ b/firmware/common/lcd_ili9341.cpp
@@ -297,6 +297,15 @@ void ILI9341::fill_rectangle(ui::Rect r, const ui::Color c) {
 	}
 }
 
+void ILI9341::fill_rectangle_unrolled8(ui::Rect r, const ui::Color c) {
+	const auto r_clipped = r.intersect(screen_rect());
+	if( !r_clipped.is_empty() ) {
+		lcd_start_ram_write(r_clipped);
+		size_t count = r_clipped.width() * r_clipped.height();
+		io.lcd_write_pixels_unrolled8(c, count);
+	}
+}
+
 void ILI9341::render_line(const ui::Point p, const uint8_t count, const ui::Color* line_buffer) {
 	lcd_start_ram_write(p, { count, 1 });
 	io.lcd_write_pixels(line_buffer, count);
diff --git a/firmware/common/lcd_ili9341.hpp b/firmware/common/lcd_ili9341.hpp
index 817d0a75a21f8da314c586315ae0f2f8ed2dedd8..ddcb12ef992e63e4fbd4150e91fc71675899f378 100644
--- a/firmware/common/lcd_ili9341.hpp
+++ b/firmware/common/lcd_ili9341.hpp
@@ -49,6 +49,7 @@ public:
 	void wake();
 
 	void fill_rectangle(ui::Rect r, const ui::Color c);
+	void fill_rectangle_unrolled8(ui::Rect r, const ui::Color c);
 	void draw_line(const ui::Point start, const ui::Point end, const ui::Color color);
 	void fill_circle(
 		const ui::Point center,
diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp
index 37ba5af3f7942f7e9e83fca8b299ecc20dc3d552..3f2a1752f8ac30f1263b228dfbfa42f91afafbfe 100644
--- a/firmware/common/message.hpp
+++ b/firmware/common/message.hpp
@@ -106,6 +106,7 @@ public:
 		
 		AudioLevelReport = 51,
 		CodedSquelch = 52,
+		AudioSpectrumConfig = 53,
 		MAX
 	};
 
@@ -273,6 +274,27 @@ public:
 	size_t trigger { 0 };
 };
 
+struct AudioSpectrum {
+	std::array<uint8_t, 128> db { { 0 } };
+	//uint32_t sampling_rate { 0 };
+};
+
+using AudioSpectrumFIFO = FIFO<AudioSpectrum>;
+
+class AudioSpectrumConfigMessage : public Message {
+public:
+	static constexpr size_t fifo_k = 2;
+	
+	constexpr AudioSpectrumConfigMessage(
+		AudioSpectrumFIFO* fifo
+	) : Message { ID::AudioSpectrumConfig },
+		fifo { fifo }
+	{
+	}
+
+	AudioSpectrumFIFO* fifo { nullptr };
+};
+
 struct ChannelSpectrum {
 	std::array<uint8_t, 256> db { { 0 } };
 	uint32_t sampling_rate { 0 };
diff --git a/firmware/common/portapack_io.hpp b/firmware/common/portapack_io.hpp
index a698c1b44ca40c864cccdce8609db1adfc94f832..a8eb9a2b17a64240b742f7709966fc776f04daf2 100644
--- a/firmware/common/portapack_io.hpp
+++ b/firmware/common/portapack_io.hpp
@@ -158,6 +158,21 @@ public:
 		}
 	}
 
+	void lcd_write_pixels_unrolled8(const ui::Color pixel, size_t n) {
+		auto v = pixel.v;
+		n >>= 3;
+		while(n--) {
+			lcd_write_data(v);
+			lcd_write_data(v);
+			lcd_write_data(v);
+			lcd_write_data(v);
+			lcd_write_data(v);
+			lcd_write_data(v);
+			lcd_write_data(v);
+			lcd_write_data(v);
+		}
+	}
+	
 	void lcd_write_pixels(const ui::Color* const pixels, size_t n) {
 		for(size_t i=0; i<n; i++) {
 			lcd_write_pixel(pixels[i]);
diff --git a/firmware/common/ui_painter.cpp b/firmware/common/ui_painter.cpp
index 90f22bbb24be25fabe4d92814e2849000e71282b..2cb0d93f56a82f96532c2be4c212ba090933effa 100644
--- a/firmware/common/ui_painter.cpp
+++ b/firmware/common/ui_painter.cpp
@@ -98,6 +98,10 @@ void Painter::fill_rectangle(const Rect r, const Color c) {
 	display.fill_rectangle(r, c);
 }
 
+void Painter::fill_rectangle_unrolled8(const Rect r, const Color c) {
+	display.fill_rectangle_unrolled8(r, c);
+}
+
 void Painter::paint_widget_tree(Widget* const w) {
 	if( ui::is_dirty() ) {
 		paint_widget(w);
diff --git a/firmware/common/ui_painter.hpp b/firmware/common/ui_painter.hpp
index b2ea26626cca0e59fd74663c2dfb8a3806d08b37..369e3a57ac9bb0b8ad5ea6baa762dce0cfd129ad 100644
--- a/firmware/common/ui_painter.hpp
+++ b/firmware/common/ui_painter.hpp
@@ -56,6 +56,7 @@ public:
 
 	void draw_rectangle(const Rect r, const Color c);
 	void fill_rectangle(const Rect r, const Color c);
+	void fill_rectangle_unrolled8(const Rect r, const Color c);
 
 	void paint_widget_tree(Widget* const w);
 	
diff --git a/firmware/common/ui_widget.cpp b/firmware/common/ui_widget.cpp
index 756ac5133d64a5f1ceae0ec67fb3c492db63d463..a456fceec3293bbf2793263226f63d2dd57279ad 100644
--- a/firmware/common/ui_widget.cpp
+++ b/firmware/common/ui_widget.cpp
@@ -1493,6 +1493,7 @@ Waveform::Waveform(
 	color_ { color }
 {
 	//set_focusable(false);
+	//previous_data.resize(length_, 0);
 }
 
 void Waveform::set_cursor(const uint32_t i, const int16_t position) {
@@ -1522,19 +1523,31 @@ void Waveform::set_length(const uint32_t new_length) {
 void Waveform::paint(Painter& painter) {
 	size_t n;
 	Coord y, y_offset = screen_rect().location().y();
-	Coord prev_x = screen_rect().location().x(), prev_y;
+	Coord prev_x, prev_y;
 	float x, x_inc;
 	Dim h = screen_rect().size().height();
 	const float y_scale = (float)(h - 1) / 65536.0;
 	int16_t * data_start = data_ + offset_;
 	
-	// Clear
-	painter.fill_rectangle(screen_rect(), Color::black());
-	
 	if (!length_) return;
 	
 	x_inc = (float)screen_rect().size().width() / length_;
 	
+	// Clear
+	painter.fill_rectangle_unrolled8(screen_rect(), Color::black());
+	/*prev_x = screen_rect().location().x();
+	prev_y = previous_data[0];
+	x = prev_x + x_inc;
+	for (n = 1; n < length_; n++) {
+		y = previous_data[n];
+		display.draw_line( {prev_x, prev_y}, {(Coord)x, y}, Color::black());
+		
+		prev_x = x;
+		prev_y = y;
+		x += x_inc;
+	}*/
+	prev_x = screen_rect().location().x();
+	
 	if (digital_) {
 		// Digital waveform: each value is an horizontal line
 		x = 0;
@@ -1557,8 +1570,10 @@ void Waveform::paint(Painter& painter) {
 		x = prev_x + x_inc;
 		h /= 2;
 		prev_y = y_offset + h - (*(data_start++) * y_scale);
+		//previous_data[0] = prev_y;
 		for (n = 1; n < length_; n++) {
 			y = y_offset + h - (*(data_start++) * y_scale);
+			//previous_data[n] = y;
 			display.draw_line( {prev_x, prev_y}, {(Coord)x, y}, color_);
 			
 			prev_x = x;
diff --git a/firmware/common/ui_widget.hpp b/firmware/common/ui_widget.hpp
index 49b40bd137c23168c65f987bd16e8209bcc262f3..685d8a52bfea8d629e09984e7ecfba8313451975 100644
--- a/firmware/common/ui_widget.hpp
+++ b/firmware/common/ui_widget.hpp
@@ -617,6 +617,7 @@ private:
 	const Color cursor_colors[2] = { Color::cyan(), Color::magenta() };
 	
 	int16_t * data_;
+	//std::vector<int16_t> previous_data { };
 	uint32_t length_;
 	uint32_t offset_;
 	bool digital_ { false };