diff --git a/firmware/application/CMakeLists.txt b/firmware/application/CMakeLists.txt
index ae1ab6e8db709dbb4bfa376b14ed842f82c1b84e..a3a3c1b3532c4aeb0f5539cb4d8ab35bdf2e2c66 100644
--- a/firmware/application/CMakeLists.txt
+++ b/firmware/application/CMakeLists.txt
@@ -36,7 +36,7 @@ set(USE_OPT "-Os -g --specs=nano.specs")
 set(USE_COPT "-std=gnu99")
 
 # C++ specific options here (added to USE_OPT).
-set(USE_CPPOPT "-std=c++14 -fno-rtti -fno-exceptions -Weffc++ -Wuninitialized")
+set(USE_CPPOPT "-std=c++17 -fno-rtti -fno-exceptions -Weffc++ -Wuninitialized")
 
 # Enable this if you want the linker to remove unused code and data
 set(USE_LINK_GC yes)
@@ -238,7 +238,6 @@ set(CPPSRC
 	apps/ui_settings.cpp
 	apps/ui_siggen.cpp
 	apps/ui_sonde.cpp
-	apps/ui_soundboard.cpp
 	apps/ui_sstvtx.cpp
 	# apps/ui_test.cpp
 	apps/ui_tone_search.cpp
@@ -247,13 +246,14 @@ set(CPPSRC
 	apps/ui_view_wav.cpp
 	apps/ui_whipcalc.cpp
 	apps/acars_app.cpp
-	apps/analog_audio_app.cpp
 	apps/ais_app.cpp
-	apps/tpms_app.cpp
-	apps/pocsag_app.cpp
-	apps/ert_app.cpp
+	apps/analog_audio_app.cpp
 	apps/capture_app.cpp
+	apps/ert_app.cpp
+	apps/pocsag_app.cpp
 	apps/replay_app.cpp
+	apps/soundboard_app.cpp
+	apps/tpms_app.cpp
 	protocols/aprs.cpp
 	protocols/ax25.cpp
 	protocols/bht.cpp
diff --git a/firmware/application/apps/capture_app.cpp b/firmware/application/apps/capture_app.cpp
index 9b4feecd1d82101454c4a98eec12160bf2c0ff7b..3e9bf862288d32fd900c839106a44445681974ba 100644
--- a/firmware/application/apps/capture_app.cpp
+++ b/firmware/application/apps/capture_app.cpp
@@ -27,9 +27,6 @@
 #include "portapack.hpp"
 using namespace portapack;
 
-#include "portapack_persistent_memory.hpp"
-using namespace portapack;
-
 namespace ui {
 
 CaptureAppView::CaptureAppView(NavigationView& nav) {
@@ -49,20 +46,20 @@ CaptureAppView::CaptureAppView(NavigationView& nav) {
 		&waterfall,
 	});
 
-	field_frequency.set_value(target_frequency());
+	field_frequency.set_value(receiver_model.tuning_frequency());
 	field_frequency.set_step(receiver_model.frequency_step());
 	field_frequency.on_change = [this](rf::Frequency f) {
-		this->on_target_frequency_changed(f);
+		this->on_tuning_frequency_changed(f);
 	};
 	field_frequency.on_edit = [this, &nav]() {
 		// TODO: Provide separate modal method/scheme?
-		auto new_view = nav.push<FrequencyKeypadView>(this->target_frequency());
+		auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
 		new_view->on_changed = [this](rf::Frequency f) {
-			this->on_target_frequency_changed(f);
+			this->on_tuning_frequency_changed(f);
 			this->field_frequency.set_value(f);
 		};
 	};
-
+	
 	field_frequency_step.set_by_value(receiver_model.frequency_step());
 	field_frequency_step.on_change = [this](size_t, OptionsField::value_t v) {
 		receiver_model.set_frequency_step(v);
@@ -70,26 +67,19 @@ CaptureAppView::CaptureAppView(NavigationView& nav) {
 	};
 	
 	option_bandwidth.on_change = [this](size_t, uint32_t base_rate) {
-		sampling_rate = 8 * base_rate;
+		sampling_rate = 8 * base_rate;	// Decimation by 8 done on baseband side
 		
 		waterfall.on_hide();
-		set_target_frequency(target_frequency());
 		record_view.set_sampling_rate(sampling_rate);
-		radio::set_baseband_rate(sampling_rate);
+		receiver_model.set_sampling_rate(sampling_rate);
 		waterfall.on_show();
 	};
-
-	radio::enable({
-		tuning_frequency(),
-		sampling_rate,
-		baseband_bandwidth,
-		rf::Direction::Receive,
-		receiver_model.rf_amp(),
-		static_cast<int8_t>(receiver_model.lna()),
-		static_cast<int8_t>(receiver_model.vga()),
-	});
 	
 	option_bandwidth.set_selected_index(7);		// 500k
+	
+	receiver_model.set_modulation(ReceiverModel::Mode::Capture);
+	receiver_model.set_baseband_bandwidth(baseband_bandwidth);
+	receiver_model.enable();
 
 	record_view.on_error = [&nav](std::string message) {
 		nav.display_modal("Error", message);
@@ -97,8 +87,7 @@ CaptureAppView::CaptureAppView(NavigationView& nav) {
 }
 
 CaptureAppView::~CaptureAppView() {
-	radio::disable();
-
+	receiver_model.disable();
 	baseband::shutdown();
 }
 
@@ -120,21 +109,8 @@ void CaptureAppView::focus() {
 	record_view.focus();
 }
 
-void CaptureAppView::on_target_frequency_changed(rf::Frequency f) {
-	set_target_frequency(f);
-}
-
-void CaptureAppView::set_target_frequency(const rf::Frequency new_value) {
-	persistent_memory::set_tuned_frequency(new_value);;
-	radio::set_tuning_frequency(tuning_frequency());
-}
-
-rf::Frequency CaptureAppView::target_frequency() const {
-	return persistent_memory::tuned_frequency();
-}
-
-rf::Frequency CaptureAppView::tuning_frequency() const {
-	return target_frequency() - (sampling_rate / 4);
+void CaptureAppView::on_tuning_frequency_changed(rf::Frequency f) {
+	receiver_model.set_tuning_frequency(f);
 }
 
 } /* namespace ui */
diff --git a/firmware/application/apps/capture_app.hpp b/firmware/application/apps/capture_app.hpp
index a91289c33d36af37a53025e9ee250d20d2a92c41..944b048c67c8745150a3f6bfe843bf88b5ba6111 100644
--- a/firmware/application/apps/capture_app.hpp
+++ b/firmware/application/apps/capture_app.hpp
@@ -29,9 +29,6 @@
 #include "ui_record_view.hpp"
 #include "ui_spectrum.hpp"
 
-#include <string>
-#include <memory>
-
 namespace ui {
 
 class CaptureAppView : public View {
@@ -53,12 +50,7 @@ private:
 	uint32_t sampling_rate = 0;
 	static constexpr uint32_t baseband_bandwidth = 2500000;
 
-	void on_target_frequency_changed(rf::Frequency f);
-
-	rf::Frequency target_frequency() const;
-	void set_target_frequency(const rf::Frequency new_value);
-
-	rf::Frequency tuning_frequency() const;
+	void on_tuning_frequency_changed(rf::Frequency f);
 
 	Labels labels {
 		{ { 0 * 8, 1 * 16 }, "Rate:", Color::light_grey() },
diff --git a/firmware/application/apps/soundboard_app.cpp b/firmware/application/apps/soundboard_app.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3c267a4bcf2775e7b033a7f38d551d5995f2ffb2
--- /dev/null
+++ b/firmware/application/apps/soundboard_app.cpp
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
+ * Copyright (C) 2016 Furrtek
+ *
+ * This file is part of PortaPack.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street,
+ * Boston, MA 02110-1301, USA.
+ */
+
+// To prepare samples: for f in ./*.wav; do sox "$f" -r 48000 -c 1 -b8 --norm "conv/$f"; done
+
+#include "soundboard_app.hpp"
+#include "string_format.hpp"
+#include "tonesets.hpp"
+
+using namespace tonekey;
+using namespace portapack;
+
+namespace ui {
+
+bool SoundBoardView::is_active() const {
+	return (bool)replay_thread;
+}
+
+void SoundBoardView::stop() {
+	if (is_active())
+		replay_thread.reset();
+	
+	transmitter_model.disable();
+	tx_view.set_transmitting(false);
+	
+	//button_play.set_bitmap(&bitmap_play);
+	ready_signal = false;
+}
+
+void SoundBoardView::handle_replay_thread_done(const uint32_t return_code) {
+	stop();
+	progressbar.set_value(0);
+	
+	if (return_code == ReplayThread::END_OF_FILE) {
+		if (check_random.value()) {
+			lfsr_v = lfsr_iterate(lfsr_v);
+			playing_id = lfsr_v % file_list.size();
+			menu_view.set_highlighted(playing_id);
+			start_tx(playing_id);
+		} else if (check_loop.value()) {
+			start_tx(playing_id);
+		}
+	} else if (return_code == ReplayThread::READ_ERROR) {
+		file_error();
+	}
+}
+
+void SoundBoardView::set_ready() {
+	ready_signal = true;
+}
+
+void SoundBoardView::focus() {
+	menu_view.focus();
+}
+
+void SoundBoardView::file_error() {
+	nav_.display_modal("Error", "File read error.");
+}
+
+void SoundBoardView::start_tx(const uint32_t id) {
+	auto reader = std::make_unique<WAVFileReader>();
+	uint32_t tone_key_index = options_tone_key.selected_index();
+	uint32_t sample_rate;
+	
+	stop();
+
+	if (!reader->open(u"/WAV/" + file_list[id].native())) {
+		file_error();
+		return;
+	}
+	
+	playing_id = id;
+	
+	progressbar.set_max(reader->sample_count());
+	
+	//button_play.set_bitmap(&bitmap_stop);
+	
+	sample_rate = reader->sample_rate();
+	
+	replay_thread = std::make_unique<ReplayThread>(
+		std::move(reader),
+		read_size, buffer_count,
+		&ready_signal,
+		[](uint32_t return_code) {
+			ReplayThreadDoneMessage message { return_code };
+			EventDispatcher::send_message(message);
+		}
+	);
+	
+	baseband::set_audiotx_config(
+		1536000 / 20,		// Update vu-meter at 20Hz
+		transmitter_model.channel_bandwidth(),
+		0,	// Gain is unused
+		TONES_F2D(tone_key_frequency(tone_key_index), 1536000)
+	);
+	baseband::set_sample_rate(sample_rate);
+	
+	transmitter_model.set_sampling_rate(1536000);
+	transmitter_model.set_baseband_bandwidth(1750000);
+	transmitter_model.enable();
+	
+	tx_view.set_transmitting(true);
+}
+
+/*void SoundBoardView::show_infos() {
+	if (!reader->open(file_list[menu_view.highlighted_index()]))
+		return;
+	
+	text_duration.set(to_string_time_ms(reader->ms_duration()));
+	text_title.set(reader->title().substr(0, 15));
+}*/
+
+void SoundBoardView::on_tx_progress(const uint32_t progress) {
+	progressbar.set_value(progress);
+}
+
+void SoundBoardView::on_select_entry() {
+	tx_view.focus();
+}
+
+void SoundBoardView::refresh_list() {
+	auto reader = std::make_unique<WAVFileReader>();
+	
+	file_list.clear();
+	
+	// List directories and files, put directories up top
+	for (const auto& entry : std::filesystem::directory_iterator(u"WAV", u"*")) {
+		if (std::filesystem::is_regular_file(entry.status())) {
+			if (entry.path().string().length()) {
+			
+				auto entry_extension = entry.path().extension().string();
+			
+				for (auto &c: entry_extension)
+					c = toupper(c);
+				
+				if (entry_extension == ".WAV") {
+					
+					if (reader->open(u"/WAV/" + entry.path().native())) {
+						if ((reader->channels() == 1) && (reader->bits_per_sample() == 8)) {
+							//sounds[c].ms_duration = reader->ms_duration();
+							//sounds[c].path = u"WAV/" + entry.path().native();
+							file_list.push_back(entry.path());
+							if (file_list.size() == 100)
+								break;
+						}
+					}
+				}
+			}
+		}
+	}
+	
+	if (!file_list.size()) {
+		// Hide widgets, show warning
+		menu_view.hidden(true);
+		text_empty.hidden(false);
+		set_dirty();
+	} else {
+		// Hide warning, show widgets
+		menu_view.hidden(false);
+		text_empty.hidden(true);
+		set_dirty();
+	
+		menu_view.clear();
+		
+		for (size_t n = 0; n < file_list.size(); n++) {
+			menu_view.add_item({
+				file_list[n].string().substr(0, 30),
+				ui::Color::white(),
+				nullptr,
+				[this](){
+					on_select_entry();
+				}
+			});
+		}
+		
+		menu_view.set_highlighted(0);	// Refresh
+	}
+}
+
+SoundBoardView::SoundBoardView(
+	NavigationView& nav
+) : nav_ (nav)
+{
+	baseband::run_image(portapack::spi_flash::image_tag_audio_tx);
+	
+	add_children({
+		&labels,
+		&menu_view,
+		&text_empty,
+		&options_tone_key,
+		&text_title,
+		&text_duration,
+		&progressbar,
+		&check_loop,
+		&check_random,
+		&tx_view
+	});
+	
+	refresh_list();
+	
+	text_title.set(to_string_dec_uint(file_list.size()));
+	
+	tone_keys_populate(options_tone_key);
+	options_tone_key.set_selected_index(0);
+	
+	check_loop.set_value(false);
+	check_random.set_value(false);
+
+	tx_view.on_edit_frequency = [this, &nav]() {
+		auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
+		new_view->on_changed = [this](rf::Frequency f) {
+			transmitter_model.set_tuning_frequency(f);
+		};
+	};
+	
+	tx_view.on_start = [this]() {
+		start_tx(menu_view.highlighted_index());
+	};
+	
+	tx_view.on_stop = [this]() {
+		tx_view.set_transmitting(false);
+		stop();
+	};
+}
+
+SoundBoardView::~SoundBoardView() {
+	transmitter_model.disable();
+	baseband::shutdown();
+}
+
+}
diff --git a/firmware/application/apps/soundboard_app.hpp b/firmware/application/apps/soundboard_app.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..2bd64b607f9b02d4a3a636d310f0b368aef71bfb
--- /dev/null
+++ b/firmware/application/apps/soundboard_app.hpp
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
+ * Copyright (C) 2016 Furrtek
+ *
+ * This file is part of PortaPack.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#ifndef __UI_SOUNDBOARD_H__
+#define __UI_SOUNDBOARD_H__
+
+#include "ui_widget.hpp"
+#include "ui_transmitter.hpp"
+#include "replay_thread.hpp"
+#include "baseband_api.hpp"
+#include "lfsr_random.hpp"
+#include "io_wave.hpp"
+#include "tone_key.hpp"
+
+namespace ui {
+
+class SoundBoardView : public View {
+public:
+	SoundBoardView(NavigationView& nav);
+	~SoundBoardView();
+
+	SoundBoardView(const SoundBoardView&) = delete;
+	SoundBoardView(SoundBoardView&&) = delete;
+	SoundBoardView& operator=(const SoundBoardView&) = delete;
+	SoundBoardView& operator=(SoundBoardView&&) = delete;
+
+	void focus() override;
+	
+	std::string title() const override { return "Soundboard"; };
+	
+private:
+	NavigationView& nav_;
+	
+	enum tx_modes {
+		NORMAL = 0,
+		RANDOM
+	};
+	
+	tx_modes tx_mode = NORMAL;
+	
+	uint32_t playing_id { };
+	
+	std::vector<std::filesystem::path> file_list { };
+
+	const size_t read_size { 2048 };	// Less ?
+	const size_t buffer_count { 3 };
+	std::unique_ptr<ReplayThread> replay_thread { };
+	bool ready_signal { false };
+	lfsr_word_t lfsr_v = 1;
+	
+	//void show_infos();
+	void start_tx(const uint32_t id);
+	//void on_ctcss_changed(uint32_t v);
+	void stop();
+	bool is_active() const;
+	void set_ready();
+	void handle_replay_thread_done(const uint32_t return_code);
+	void file_error();
+	void on_tx_progress(const uint32_t progress);
+	void refresh_list();
+	void on_select_entry();
+	
+	Labels labels {
+		{ { 0, 20 * 8 + 4 }, "Title:", Color::light_grey() },
+		{ { 0, 23 * 8 }, "Key:", Color::light_grey() }
+	};
+	
+	MenuView menu_view {
+		{ 0, 2 * 8, 240, 20 * 8 },
+		true
+	};
+	Text text_empty {
+		{ 7 * 8, 12 * 8, 16 * 8, 16 },
+		"Empty directory !",
+	};
+	
+	Text text_title {
+		{ 6 * 8, 20 * 8 + 4, 15 * 8, 16 }
+	};
+	
+	Text text_duration {
+		{ 22 * 8, 20 * 8 + 4, 6 * 8, 16 }
+	};
+	
+	OptionsField options_tone_key {
+		{ 4 * 8, 23 * 8 },
+		18,
+		{ }
+	};
+	
+	Checkbox check_loop {
+		{ 8, 25 * 8 + 4 },
+		4,
+		"Loop"
+	};
+	
+	Checkbox check_random {
+		{ 10 * 8, 25 * 8 + 4 },
+		6,
+		"Random"
+	};
+	
+	ProgressBar progressbar {
+		{ 0 * 8, 30 * 8 - 4, 30 * 8, 16 }
+	};
+	
+	TransmitterView tx_view {
+		16 * 16,
+		5000,
+		12
+	};
+	
+	MessageHandlerRegistration message_handler_replay_thread_error {
+		Message::ID::ReplayThreadDone,
+		[this](const Message* const p) {
+			const auto message = *reinterpret_cast<const ReplayThreadDoneMessage*>(p);
+			this->handle_replay_thread_done(message.return_code);
+		}
+	};
+	
+	MessageHandlerRegistration message_handler_fifo_signal {
+		Message::ID::RequestSignal,
+		[this](const Message* const p) {
+			const auto message = static_cast<const RequestSignalMessage*>(p);
+			if (message->signal == RequestSignalMessage::Signal::FillRequest) {
+				this->set_ready();
+			}
+		}
+	};
+	
+	MessageHandlerRegistration message_handler_tx_progress {
+		Message::ID::TXProgress,
+		[this](const Message* const p) {
+			const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
+			this->on_tx_progress(message.progress);
+		}
+	};
+};
+
+} /* namespace ui */
+
+#endif/*__UI_SOUNDBOARD_H__*/
diff --git a/firmware/application/clock_manager.cpp b/firmware/application/clock_manager.cpp
index b808c1cdc220278b1d5d7aec9dd8232feda829e8..fcddba0f5a50a58843210492729e82f0b60bb9eb 100644
--- a/firmware/application/clock_manager.cpp
+++ b/firmware/application/clock_manager.cpp
@@ -247,10 +247,10 @@ constexpr ClockControls si5351_clock_control_clkin {
 	si5351_clock_control_common[4] | si5351_clock_control_ms_src_clkin,
 	si5351_clock_control_common[5] | si5351_clock_control_ms_src_clkin,
 	si5351_clock_control_common[6] | si5351_clock_control_ms_src_clkin,
-	si5351_clock_control_common[7] | si5351_clock_control_ms_src_xtal,
+	si5351_clock_control_common[7] | si5351_clock_control_ms_src_clkin,
 };
 
-void ClockManager::init(const bool use_clkin) {
+void ClockManager::init() {
 	/* Must be sure to run the M4 core from IRC when messing with the signal
 	 * generator that sources the GP_CLKIN signal that drives the micro-
 	 * controller's PLL1 input.
@@ -269,11 +269,23 @@ void ClockManager::init(const bool use_clkin) {
 	clock_generator.enable_fanout();
 	clock_generator.set_pll_input_sources(si5351_pll_input_sources);
 
-	//const bool use_clkin = false;
+	const auto clkin_present = !clock_generator.clkin_loss_of_signal();
+	auto clkin_valid = false;
+
+	if( clkin_present ) {
+		// Measure Si5351B CLKIN frequency against LPC43xx IRC oscillator
+		set_gp_clkin_to_clkin_direct();
+		start_frequency_monitor_measurement(cgu::CLK_SEL::GP_CLKIN);
+		wait_For_frequency_monitor_measurement_done();
+		const auto clkin_frequency = get_frequency_monitor_measurement_in_hertz();
+
+		// CLKIN is required to be 10MHz. FREQ_MON measurement is accurate to 1.5%
+		// due to LPC43xx IRC oscillator precision.
+		clkin_valid = (clkin_frequency >= 9850000) && (clkin_frequency <= 10150000);
+	}
+
 	clock_generator.set_clock_control(
-		use_clkin ?
-			si5351_clock_control_clkin
-		: si5351_clock_control_xtal
+		clkin_valid ? si5351_clock_control_clkin : si5351_clock_control_xtal
 	);
 
 	clock_generator.write(si5351_pll_a_xtal_reg);
@@ -422,6 +434,38 @@ void ClockManager::disable_gp_clkin_source() {
 	clock_generator.disable_output(clock_generator_output_mcu_clkin);
 }
 
+void ClockManager::set_gp_clkin_to_clkin_direct() {
+	clock_generator.set_clock_control(
+		clock_generator_output_mcu_clkin,
+		{ ClockControl::CLK_IDRV_2mA | ClockControl::CLK_SRC_CLKIN | ClockControl::CLK_INV_Normal | ClockControl::MS_INT_Integer | ClockControl::CLK_PDN_Power_On }
+	);
+	enable_gp_clkin_source();
+}
+
+void ClockManager::start_frequency_monitor_measurement(const cgu::CLK_SEL clk_sel) {
+	// Measure a clock input for 480 cycles of the LPC43xx IRC.
+	LPC_CGU->FREQ_MON = LPC_CGU_FREQ_MON_Type {
+		.RCNT = 480,
+		.FCNT = 0,
+		.MEAS = 0,
+		.CLK_SEL = toUType(clk_sel),
+		.RESERVED0 = 0
+	};
+	LPC_CGU->FREQ_MON.MEAS = 1;
+}
+
+void ClockManager::wait_For_frequency_monitor_measurement_done() {
+	// FREQ_MON mechanism fails to finish if there's no clock present on selected input?!
+	while(LPC_CGU->FREQ_MON.MEAS == 1);
+}
+
+uint32_t ClockManager::get_frequency_monitor_measurement_in_hertz() {
+	// Measurement is only as accurate as the LPC43xx IRC oscillator,
+	// which is +/- 1.5%. Measurement is for 480 IRC clcocks. Scale
+	// the cycle count to get a value in Hertz.
+	return LPC_CGU->FREQ_MON.FCNT * 25000;
+}
+
 void ClockManager::enable_xtal_oscillator() {
 	LPC_CGU->XTAL_OSC_CTRL.BYPASS = 0;
 	LPC_CGU->XTAL_OSC_CTRL.ENABLE = 1;
diff --git a/firmware/application/clock_manager.hpp b/firmware/application/clock_manager.hpp
index 172dbcb7800701c779de714ce32234eb72352529..5d4799eea344b8eefc0364a807822da6e83411b5 100644
--- a/firmware/application/clock_manager.hpp
+++ b/firmware/application/clock_manager.hpp
@@ -42,7 +42,7 @@ public:
 	{
 	}
 
-	void init(const bool use_clkin);
+	void init();
 	void shutdown();
 
 	void run_from_irc();
@@ -66,6 +66,8 @@ public:
 
 	void set_reference_ppb(const int32_t ppb);
 
+	uint32_t get_frequency_monitor_measurement_in_hertz();
+
 private:
 	I2C& i2c0;
 	si5351::Si5351& clock_generator;
@@ -75,6 +77,10 @@ private:
 
 	void enable_gp_clkin_source();
 	void disable_gp_clkin_source();
+	void set_gp_clkin_to_clkin_direct();
+
+	void start_frequency_monitor_measurement(const cgu::CLK_SEL clk_sel);
+	void wait_For_frequency_monitor_measurement_done();
 
 	void enable_xtal_oscillator();
 	void disable_xtal_oscillator();
diff --git a/firmware/application/hw/si5351.hpp b/firmware/application/hw/si5351.hpp
index e4c622b67bd40a6d9649db181cbeffd5767fff5e..c7508ba9e6af573d56fe2d2b8c497fd77e8d6432 100644
--- a/firmware/application/hw/si5351.hpp
+++ b/firmware/application/hw/si5351.hpp
@@ -306,6 +306,10 @@ public:
 		while(device_status() & 0x80);
 	}
 
+	bool clkin_loss_of_signal() {
+		return (device_status() >> 4) & 1;
+	}
+	
 	void enable_fanout() {
 		write_register(Register::FanoutEnable, 0b11010000);
 	}
@@ -369,6 +373,11 @@ public:
 		update_all_clock_control();
 	}
 
+	void set_clock_control(const size_t n, const ClockControl::Type clock_control) {
+		_clock_control[n] = clock_control;
+		write_register(Register::CLKControl_Base + n, _clock_control[n]);
+	}
+
 	void enable_clock(const size_t n) {
 		_clock_control[n] &= ~ClockControl::CLK_PDN_Mask;
 		write_register(Register::CLKControl_Base + n, _clock_control[n]);
diff --git a/firmware/application/portapack.cpp b/firmware/application/portapack.cpp
index eaaef11379bf41553d6cc7a99d920feb231203ee..04dac6df7cb9804882dccb73054cf0e4cbf362c1 100644
--- a/firmware/application/portapack.cpp
+++ b/firmware/application/portapack.cpp
@@ -104,7 +104,7 @@ void poll_ext_clock() {
 	if (clkin_status != prev_clkin_status) {
 		StatusRefreshMessage message { };
 		EventDispatcher::send_message(message);
-		clock_manager.init(clkin_status);
+		clock_manager.init();
 	}
 	
 	prev_clkin_status = clkin_status;
@@ -296,7 +296,7 @@ bool init() {
 	led_rx.setup();
 	led_tx.setup();
 
-	clock_manager.init(false);
+	clock_manager.init();
 	clock_manager.set_reference_ppb(persistent_memory::correction_ppb());
 	clock_manager.run_at_full_speed();
 
diff --git a/firmware/application/receiver_model.cpp b/firmware/application/receiver_model.cpp
index c8b49967917e49e085654abe250b8395e8df5e7a..782ab94a782b93e2bea32cbb4b4b4765162921f1 100644
--- a/firmware/application/receiver_model.cpp
+++ b/firmware/application/receiver_model.cpp
@@ -275,6 +275,7 @@ void ReceiverModel::update_modulation() {
 		break;
 
 	case Mode::SpectrumAnalysis:
+	case Mode::Capture:
 		break;
 	}
 }
diff --git a/firmware/application/receiver_model.hpp b/firmware/application/receiver_model.hpp
index 139f0d67e6554e148892925affcb2a486b8df02c..5b2a82f361c037080f19c469df282d243bdd8908 100644
--- a/firmware/application/receiver_model.hpp
+++ b/firmware/application/receiver_model.hpp
@@ -37,6 +37,7 @@ public:
 		NarrowbandFMAudio = 1,
 		WidebandFMAudio = 2,
 		SpectrumAnalysis = 3,
+		Capture = 4
 	};
 
 	rf::Frequency tuning_frequency() const;
diff --git a/firmware/application/tone_key.cpp b/firmware/application/tone_key.cpp
index b53c074983a1dbccd043f76220685e1db89f66b6..a208c96e1eb1e14201cc3dbf8efe8d56a3867907 100644
--- a/firmware/application/tone_key.cpp
+++ b/firmware/application/tone_key.cpp
@@ -78,8 +78,8 @@ const tone_key_t tone_keys = {
 	{ "38 --", 250.300 },
 	{ "50 0Z", 254.100 },
 	{ "Axient 28kHz", 28000.0 },
-	{ "Sennheiser 32.768k", 32768.0 },
-	{ "Sennheiser 32.000k", 32000.0 },
+	{ "Senn. 32.768k", 32768.0 },
+	{ "Senn. 32.000k", 32000.0 },
 	{ "Sony 32.382k", 32382.0 },
 	{ "Shure 19kHz", 19000.0 }
 };
diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp
index 6ebb10aa533283fb1e28c90acdf7c6836865fd8d..34e765dceeb0443befc4be958f48d5c65c802c02 100644
--- a/firmware/application/ui_navigation.cpp
+++ b/firmware/application/ui_navigation.cpp
@@ -58,7 +58,6 @@
 #include "ui_settings.hpp"
 #include "ui_siggen.hpp"
 #include "ui_sonde.hpp"
-#include "ui_soundboard.hpp"
 #include "ui_sstvtx.hpp"
 //#include "ui_test.hpp"
 #include "ui_tone_search.hpp"
@@ -73,6 +72,7 @@
 #include "ert_app.hpp"
 #include "pocsag_app.hpp"
 #include "replay_app.hpp"
+#include "soundboard_app.hpp"
 #include "tpms_app.hpp"
 
 #include "core_control.hpp"
@@ -166,6 +166,8 @@ void SystemStatusView::refresh() {
 		image_clock_status.set_bitmap(&bitmap_icon_clk_int);
 		button_bias_tee.set_foreground(ui::Color::light_grey());
 	}
+	
+	set_dirty();
 }
 
 void SystemStatusView::set_back_enabled(bool new_value) {
@@ -186,9 +188,7 @@ void SystemStatusView::on_stealth() {
 	
 	portapack::persistent_memory::set_stealth_mode(mode);
 
-	button_stealth.set_foreground(mode ? ui::Color::green() : ui::Color::light_grey());
-	
-	button_stealth.set_dirty();
+	button_stealth.set_foreground(mode ? Color::green() : Color::light_grey());
 }
 
 void SystemStatusView::on_bias_tee() {
@@ -196,6 +196,7 @@ void SystemStatusView::on_bias_tee() {
 		nav_.display_modal("Bias voltage", "Enable DC voltage on\nantenna connector ?", YESNO, [this](bool v) {
 				if (v) {
 					portapack::set_antenna_bias(true);
+					//radio::set_antenna_bias(true);
 					receiver_model.set_antenna_bias();
 					transmitter_model.set_antenna_bias();
 					refresh();
@@ -203,6 +204,7 @@ void SystemStatusView::on_bias_tee() {
 			});
 	} else {
 		portapack::set_antenna_bias(false);
+		//radio::set_antenna_bias(false);
 		receiver_model.set_antenna_bias();
 		transmitter_model.set_antenna_bias();
 		refresh();
@@ -371,7 +373,7 @@ TransmittersMenuView::TransmittersMenuView(NavigationView& nav) {
 		{ "Key fob", 				ui::Color::orange(),	&bitmap_icon_keyfob,	[&nav](){ nav.push<KeyfobView>(); } },
 		{ "Microphone", 			ui::Color::green(),		&bitmap_icon_microphone,	[&nav](){ nav.push<MicTXView>(); } },
 		{ "Morse code", 			ui::Color::green(),		&bitmap_icon_morse,		[&nav](){ nav.push<MorseView>(); } },
-		{ "NTTWorks burger pager", 	ui::Color::yellow(), 	&bitmap_icon_burger,	[&nav](){ nav.push<CoasterPagerView>(); } },
+		{ "Burger pagers", 			ui::Color::yellow(), 	&bitmap_icon_burger,	[&nav](){ nav.push<CoasterPagerView>(); } },
 		//{ "Nuoptix DTMF timecode", 	ui::Color::green(),		&bitmap_icon_nuoptix,	[&nav](){ nav.push<NuoptixView>(); } },
 		{ "OOK encoders", 			ui::Color::yellow(),	&bitmap_icon_remote,	[&nav](){ nav.push<EncodersView>(); } },
 		{ "POCSAG", 				ui::Color::green(),		&bitmap_icon_pocsag,	[&nav](){ nav.push<POCSAGTXView>(); } },
@@ -478,10 +480,12 @@ SystemView::SystemView(
 		navigation_view.push<PlayDeadView>();
 	} else {*/
 	
+		navigation_view.push<SystemMenuView>();
+		
 		if (portapack::persistent_memory::config_splash())
 			navigation_view.push<BMPView>();
-		else
-			navigation_view.push<SystemMenuView>();
+		//else
+		//	navigation_view.push<SystemMenuView>();
 			
 	//}
 }
@@ -504,7 +508,6 @@ BMPView::BMPView(NavigationView& nav) {
 	
 	button_done.on_select = [this, &nav](Button&){
 		nav.pop();
-		nav.push<SystemMenuView>();
 	};
 }
 
diff --git a/firmware/baseband/CMakeLists.txt b/firmware/baseband/CMakeLists.txt
index 94311c06565b94890f6d31ed5935da75d64b25c2..179e0b18ce297b68be2e087b631156c49f04bb27 100644
--- a/firmware/baseband/CMakeLists.txt
+++ b/firmware/baseband/CMakeLists.txt
@@ -36,7 +36,7 @@ set(USE_OPT "-O3 -g -falign-functions=16 -fno-math-errno --specs=nano.specs")
 set(USE_COPT "-std=gnu99")
 
 # C++ specific options here (added to USE_OPT).
-set(USE_CPPOPT "-std=c++14 -fno-rtti -fno-exceptions -Weffc++ -Wuninitialized")
+set(USE_CPPOPT "-std=c++17 -fno-rtti -fno-exceptions -Weffc++ -Wuninitialized")
 
 # Enable this if you want the linker to remove unused code and data
 set(USE_LINK_GC yes)
diff --git a/firmware/baseband/proc_audiotx.cpp b/firmware/baseband/proc_audiotx.cpp
index f18a545a3a969328e78c3fbfe8e76c24924aecbd..b87cfc78627fdb1fc88f609c851ee5ab0066c022 100644
--- a/firmware/baseband/proc_audiotx.cpp
+++ b/firmware/baseband/proc_audiotx.cpp
@@ -23,7 +23,6 @@
 #include "proc_audiotx.hpp"
 #include "portapack_shared_memory.hpp"
 #include "sine_table_int8.hpp"
-//#include "audio_output.hpp"
 #include "event_m4.hpp"
 
 #include <cstdint>
@@ -32,17 +31,18 @@ void AudioTXProcessor::execute(const buffer_c8_t& buffer){
 	
 	if (!configured) return;
 	
-	if( stream ) {
-		const size_t bytes_to_read = (buffer.count / 32);	// /32 (oversampling) should be == 64
-		bytes_read += stream->read(audio_buffer.p, bytes_to_read);
-	}
-	
-	// Fill and "stretch"
+	// Zero-order hold (poop)
 	for (size_t i = 0; i < buffer.count; i++) {
-		if (!(i & 31))
-			audio_sample = audio_buffer.p[i >> 5] - 0x80;
+		resample_acc += resample_inc;
+		if (resample_acc >= 0x10000) {
+			resample_acc -= 0x10000;
+			if (stream) {
+				stream->read(&audio_sample, 1);
+				bytes_read++;
+			}
+		}
 		
-		sample = tone_gen.process(audio_sample);
+		sample = tone_gen.process(audio_sample - 0x80);
 		
 		// FM
 		delta = sample * fm_delta;
@@ -50,22 +50,20 @@ void AudioTXProcessor::execute(const buffer_c8_t& buffer){
 		phase += delta;
 		sphase = phase + (64 << 24);
 		
-		re = (sine_table_i8[(sphase & 0xFF000000U) >> 24]);
-		im = (sine_table_i8[(phase & 0xFF000000U) >> 24]);
+		re = sine_table_i8[(sphase & 0xFF000000U) >> 24];
+		im = sine_table_i8[(phase & 0xFF000000U) >> 24];
 		
 		buffer.p[i] = { (int8_t)re, (int8_t)im };
 	}
 	
-	spectrum_samples += buffer.count;
-	if( spectrum_samples >= spectrum_interval_samples ) {
-		spectrum_samples -= spectrum_interval_samples;
+	progress_samples += buffer.count;
+	if (progress_samples >= progress_interval_samples) {
+		progress_samples -= progress_interval_samples;
 		
 		txprogress_message.progress = bytes_read;	// Inform UI about progress
 		txprogress_message.done = false;
 		shared_memory.application_queue.push(txprogress_message);
 	}
-	
-	//AudioOutput::fill_audio_buffer(preview_audio_buffer, true);
 }
 
 void AudioTXProcessor::on_message(const Message* const message) {
@@ -96,12 +94,8 @@ void AudioTXProcessor::on_message(const Message* const message) {
 void AudioTXProcessor::audio_config(const AudioTXConfigMessage& message) {
 	fm_delta = message.deviation_hz * (0xFFFFFFULL / baseband_fs);
 	tone_gen.configure(message.tone_key_delta, message.tone_key_mix_weight);
-}
-
-void AudioTXProcessor::samplerate_config(const SamplerateConfigMessage& message) {
-	baseband_fs = message.sample_rate;
-	baseband_thread.set_sampling_rate(baseband_fs);
-	spectrum_interval_samples = baseband_fs / 20;
+	progress_interval_samples = message.divider;
+	resample_acc = 0;
 }
 
 void AudioTXProcessor::replay_config(const ReplayConfigMessage& message) {
@@ -116,6 +110,10 @@ void AudioTXProcessor::replay_config(const ReplayConfigMessage& message) {
 	}
 }
 
+void AudioTXProcessor::samplerate_config(const SamplerateConfigMessage& message) {
+	resample_inc = (((uint64_t)message.sample_rate) << 16) / baseband_fs;	// 16.16 fixed point message.sample_rate
+}
+
 int main() {
 	EventDispatcher event_dispatcher { std::make_unique<AudioTXProcessor>() };
 	event_dispatcher.run();
diff --git a/firmware/baseband/proc_audiotx.hpp b/firmware/baseband/proc_audiotx.hpp
index b15f8524c96a3dc68f093fe9d1d9cb9b2469ea22..3f1bace7092b71b3cf584d28af25d8a497e80897 100644
--- a/firmware/baseband/proc_audiotx.hpp
+++ b/firmware/baseband/proc_audiotx.hpp
@@ -35,35 +35,22 @@ public:
 	void on_message(const Message* const msg) override;
 
 private:
-	size_t baseband_fs = 0;
+	static constexpr size_t baseband_fs = 1536000;
 	
 	BasebandThread baseband_thread { baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Transmit };
 	
-	std::array<uint8_t, 64> audio { };
-	const buffer_t<uint8_t> audio_buffer {
-		audio.data(),
-		audio.size(),
-		baseband_fs / 8
-	};
-	
 	std::unique_ptr<StreamOutput> stream { };
 	
 	ToneGen tone_gen { };
 	
+	uint32_t resample_inc { }, resample_acc { };
 	uint32_t fm_delta { 0 };
 	uint32_t phase { 0 }, sphase { 0 };
-	int8_t out_sample { };
-	int32_t sample { 0 }, audio_sample { 0 }, delta { };
+	uint8_t audio_sample { };
+	int32_t sample { 0 }, delta { };
 	int8_t re { 0 }, im { 0 };
 	
-	size_t spectrum_interval_samples = 0;
-	size_t spectrum_samples = 0;
-
-	//int16_t audio_data[64];
-	/*const buffer_s16_t preview_audio_buffer {
-		audio_data,
-		sizeof(int16_t)*64
-	};*/
+	size_t progress_interval_samples, progress_samples = 0;
 	
 	bool configured { false };
 	uint32_t bytes_read { 0 };
diff --git a/firmware/bootstrap/CMakeLists.txt b/firmware/bootstrap/CMakeLists.txt
index 0c5e4a29a2daa420385a37678b56afb8cb5b09b9..1c3fde757b57198d659eaf3e46954cdd20cad305 100644
--- a/firmware/bootstrap/CMakeLists.txt
+++ b/firmware/bootstrap/CMakeLists.txt
@@ -35,7 +35,7 @@ set(USE_OPT "-Os -g -falign-functions=16 -fno-math-errno --specs=nano.specs")
 set(USE_COPT "-std=gnu99")
 
 # C++ specific options here (added to USE_OPT).
-set(USE_CPPOPT "-std=c++14 -fno-rtti -fno-exceptions -Weffc++ -Wuninitialized")
+set(USE_CPPOPT "-std=c++17 -fno-rtti -fno-exceptions -Weffc++ -Wuninitialized")
 
 # Enable this if you want the linker to remove unused code and data
 set(USE_LINK_GC yes)
diff --git a/firmware/chibios/os/ports/common/ARMCMx/CMSIS/include/core_cmFunc.h b/firmware/chibios/os/ports/common/ARMCMx/CMSIS/include/core_cmFunc.h
index 585d2bb56cb3f9ac2d5797a2ff0e2a5822f8239f..a260cf6bc4896ffd9ec352fa82d7c8a88c17532d 100755
--- a/firmware/chibios/os/ports/common/ARMCMx/CMSIS/include/core_cmFunc.h
+++ b/firmware/chibios/os/ports/common/ARMCMx/CMSIS/include/core_cmFunc.h
@@ -307,6 +307,10 @@ __STATIC_INLINE void __set_FPSCR(uint32_t fpscr)
 #elif defined ( __GNUC__ ) /*------------------ GNU Compiler ---------------------*/
 /* GNU gcc specific functions */
 
+#ifndef   __STATIC_FORCEINLINE
+  #define __STATIC_FORCEINLINE                   __attribute__((always_inline)) static inline
+#endif
+
 /** \brief  Enable IRQ Interrupts
 
   This function enables IRQ interrupts by clearing the I-bit in the CPSR.
@@ -403,15 +407,15 @@ __attribute__( ( always_inline ) ) __STATIC_INLINE uint32_t __get_xPSR(void)
 
 /** \brief  Get Process Stack Pointer
 
-    This function returns the current value of the Process Stack Pointer (PSP).
+    \details Returns the current value of the Process Stack Pointer (PSP).
 
     \return               PSP Register value
  */
-__attribute__( ( always_inline ) ) __STATIC_INLINE uint32_t __get_PSP(void)
+__STATIC_FORCEINLINE uint32_t __get_PSP(void)
 {
-  register uint32_t result;
+  uint32_t result;
 
-  __ASM volatile ("MRS %0, psp\n"  : "=r" (result) );
+  __ASM volatile ("MRS %0, psp"  : "=r" (result) );
   return(result);
 }
 
@@ -430,19 +434,18 @@ __attribute__( ( always_inline ) ) __STATIC_INLINE void __set_PSP(uint32_t topOf
 
 /** \brief  Get Main Stack Pointer
 
-    This function returns the current value of the Main Stack Pointer (MSP).
+    \details Returns the current value of the Main Stack Pointer (MSP).
 
     \return               MSP Register value
  */
-__attribute__( ( always_inline ) ) __STATIC_INLINE uint32_t __get_MSP(void)
+__STATIC_FORCEINLINE uint32_t __get_MSP(void)
 {
-  register uint32_t result;
+  uint32_t result;
 
-  __ASM volatile ("MRS %0, msp\n" : "=r" (result) );
+  __ASM volatile ("MRS %0, msp" : "=r" (result) );
   return(result);
 }
 
-
 /** \brief  Set Main Stack Pointer
 
     This function assigns the given value to the Main Stack Pointer (MSP).
diff --git a/firmware/common/lpc43xx_cpp.hpp b/firmware/common/lpc43xx_cpp.hpp
index 92ca64ec1b77257280c4cb5f50f9a74a6b787e2a..b4c124cd87540b01e3102dc4415b92ed37ce9d46 100644
--- a/firmware/common/lpc43xx_cpp.hpp
+++ b/firmware/common/lpc43xx_cpp.hpp
@@ -108,7 +108,7 @@ inline void clear() {
 
 namespace cgu {
 
-enum class CLK_SEL {
+enum class CLK_SEL : uint8_t {
 	RTC_32KHZ	= 0x00,
 	IRC			= 0x01,
 	ENET_RX_CLK = 0x02,
diff --git a/firmware/common/ui_widget.hpp b/firmware/common/ui_widget.hpp
index 49b40bd137c23168c65f987bd16e8209bcc262f3..db3bdda43e6d06f3be59ea3a9b37a8ef2f5e1cdd 100644
--- a/firmware/common/ui_widget.hpp
+++ b/firmware/common/ui_widget.hpp
@@ -37,6 +37,7 @@
 #include <memory>
 #include <vector>
 #include <string>
+#include <functional>
 
 namespace ui {
 
diff --git a/firmware/graphics/splash.bmp b/firmware/graphics/splash.bmp
deleted file mode 100755
index f7138934b7914aded00c783388d582565720b444..0000000000000000000000000000000000000000
Binary files a/firmware/graphics/splash.bmp and /dev/null differ