diff --git a/common/math.hpp b/common/math.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..9e62b473fcd42e683c1b1ca80fda73f0c4b8cd70
--- /dev/null
+++ b/common/math.hpp
@@ -0,0 +1,170 @@
+#ifndef COMMON_MATH_HPP
+#define COMMON_MATH_HPP
+#include "simple/support.hpp"
+#include "simple/geom.hpp"
+
+namespace common
+{
+
+using namespace simple;
+
+constexpr auto half = geom::vector<float,2>::one(.5f);
+const float tau = 2*std::acos(-1);
+
+template <typename Number, typename Ratio = float>
+[[nodiscard]] constexpr
+Number lerp(Number from, Number to, Ratio ratio)
+{
+	return from + (to - from) * ratio;
+}
+
+template <typename Vector>
+[[nodiscard]] constexpr
+Vector project_on_unit(Vector x, Vector surface)
+{
+	return surface * surface(x);
+}
+
+template <typename Vector>
+[[nodiscard]] constexpr
+Vector project(Vector x, Vector surface)
+{
+	return project_on_unit(x,surface) / surface.magnitude();
+}
+
+template <typename Vector>
+[[nodiscard]] constexpr
+Vector reject_from_unit(Vector x, Vector surface)
+{
+	return x - project_on_unit(x,surface);
+}
+
+template <typename Vector>
+[[nodiscard]] constexpr
+Vector reject(Vector x, Vector surface)
+{
+	return x - project(x,surface);
+}
+
+template <typename Vector>
+[[nodiscard]] constexpr
+Vector reflect_in_unit(Vector x, Vector surface)
+{
+	return project_on_unit(x,surface) - reject_from_unit(x,surface);
+}
+
+template <typename Vector>
+[[nodiscard]] constexpr
+Vector reflect(Vector x, Vector surface)
+{
+	return project(x,surface) - reject(x,surface);
+}
+
+template <typename Vector>
+[[nodiscard]] constexpr
+Vector rotate_scale(Vector x, Vector half_angle)
+{
+	return reflect_in_unit(
+			reflect_in_unit(x, Vector::i()),
+			half_angle );
+}
+
+template <typename Vector>
+[[nodiscard]] constexpr
+Vector rotate(Vector x, Vector half_angle)
+{
+	return reflect( reflect(x, Vector::i()), half_angle );
+}
+
+template <typename Vector>
+[[nodiscard]] constexpr
+Vector normalize(Vector x)
+{
+	return x / support::root2(x.quadrance());
+}
+
+template <size_t Exponent = 3, typename Value = float>
+class protractor
+{
+	public:
+	using vector = geom::vector<Value,2>;
+
+	using array = std::array<vector, (size_t(1)<<Exponent) + 1>;
+
+	static constexpr array circle = []()
+	{
+		using support::midpoint;
+
+		array points{};
+
+		points.front() = vector::i();
+		points.back() = -vector::i();
+
+		auto bisect = [](auto begin, auto end,
+		auto& self)
+		{
+			if(end - begin <= 2)
+				return;
+
+			auto middle = midpoint(begin, end);
+			*(middle) = normalize(midpoint(*begin, *(end - 1)));
+
+			self(begin, middle + 1,
+			self);
+			self(middle, end,
+			self);
+		};
+
+		if(Exponent > 0)
+		{
+			auto middle = midpoint(points.begin(), points.end());
+			*middle = vector::j();
+
+			bisect(points.begin(), middle + 1,
+			bisect);
+			bisect(middle, points.end(),
+			bisect);
+		}
+
+		return points;
+	}();
+
+
+	static constexpr vector tau(Value factor)
+	{
+		assert(Value{0} <= factor && factor < Value{1});
+		Value index = factor * (protractor::circle.size() - 1);
+		int whole = index;
+		Value fraction = index - whole;
+		return lerp(circle[whole], circle[whole+1], fraction);
+	}
+
+	static constexpr vector tau()
+	{
+		return -vector::i();
+	}
+
+	static constexpr vector small_radian(Value value)
+	{
+		return {support::root2(Value{1} - value*value), value};
+	}
+
+	static vector radian(Value angle)
+	{
+		return {std::cos(angle), std::sin(angle)};
+	}
+
+	static constexpr vector angle(Value tau_factor)
+	{
+		if(tau_factor < (Value{1}/(circle.size()-1))/16 )
+			return small_radian(tau_factor * common::tau);
+		else
+			return protractor::tau(tau_factor);
+	}
+
+};
+
+} // namespace common
+
+#endif /* end of include guard */
+
diff --git a/common/motion.hpp b/common/motion.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..4d1c4f1e39f473c2f897ae5501a34bc689a5f3cf
--- /dev/null
+++ b/common/motion.hpp
@@ -0,0 +1,192 @@
+// what even is a tween?
+// shorhand for between??
+// what even is between???
+
+#ifndef COMMON_MOTION_HPP
+#define COMMON_MOTION_HPP
+
+float linear_curve(float x) { return x; };
+float quadratic_curve(float x) { return x*x; };
+float cubic_curve(float x) { return x*x*x; };
+
+using curve_t = decltype(&linear_curve);
+
+struct advance_result
+{
+	bool success = false;
+	Program::duration remaining = 0s;
+	explicit operator bool() { return success; }
+};
+struct multi_advance_result : public advance_result
+{
+	range<size_t> updated = range<size_t>{0,0};
+	using advance_result::operator bool;
+};
+
+template <typename Type, curve_t curve = linear_curve>
+struct motion
+{
+	Type start;
+	Type end;
+	Program::duration total = 0s;
+	Program::duration elapsed = 0s;
+
+	decltype(auto) value()
+	{
+		auto ratio = total == 0s ? 0 : elapsed.count()/total.count();
+		return lerp(start, end, curve(ratio));
+	}
+
+	bool done()
+	{
+		return elapsed >= total;
+	}
+
+	advance_result advance(Program::duration delta)
+	{
+		elapsed += delta;
+		if(done())
+		{
+			elapsed = total;
+			return {false};
+		}
+		return {true};
+	}
+
+	void reset()
+	{
+		elapsed = 0s;
+	}
+
+	bool move(Type& target, Program::duration delta)
+	{
+		bool success = advance(delta).success;
+		target = value();
+		return !success;
+	}
+};
+
+// welcome to proper looping
+// TODO: for know total duration mod(remaining, total)
+// TODO: for unknown total while(move, !success){delta = remaining}
+template <typename Motion, typename Target>
+bool loop(Target&& target, Motion& motion, Program::duration delta)
+{
+	if(auto [success, remaining] = motion.move(std::forward<Target>(target), delta);
+		!success)
+	{
+		motion.reset();
+		motion.advance(remaining);
+		return true;
+	}
+	return false;
+}
+
+template <typename... Motions>
+class ensemble // TODO
+{
+	std::tuple<Motions...> instruments;
+	bool done();
+	bool advance(Program::duration delta);
+	void reset();
+	template <typename T>
+	bool move(T& target, Program::duration delta);
+};
+
+template <typename... Motions>
+class symphony
+{
+	std::tuple<Motions...> movements;
+	size_t current_index = 0;
+
+	template<typename F, size_t I = sizeof...(Motions) - 1>
+	void for_all(F&& f)
+	{
+		static_assert(I >= 0 && I < sizeof...(Motions));
+		std::forward<F>(f)(std::get<I>(movements));
+		if constexpr (I > 0)
+			for_all<F, I-1>(std::forward<F>(f));
+	}
+
+	multi_advance_result advance(Program::duration delta, range<size_t> updated)
+	{
+		auto [success, remaining] = support::apply_for(current_index, [&delta](auto&& movement)
+		{
+			return movement.advance(delta);
+		}, movements);
+
+		if(!success)
+		{
+			if(current_index == sizeof...(Motions) - 1)
+				return {success, remaining, updated};
+
+			++current_index;
+
+			if(remaining > 0s)
+				return advance(remaining, {updated.lower(), updated.upper()+1});
+			else
+				return {true, remaining, updated};
+		}
+		else
+		{
+			return {success, remaining, updated};
+		}
+	}
+
+	public:
+
+	symphony() = default;
+	symphony(Motions... motions) : movements{motions...} {}
+
+	bool done()
+	{
+		return std::get<sizeof...(Motions) - 1>(movements).done();
+	}
+
+	auto advance(Program::duration delta)
+	{
+		return advance(delta, {current_index, current_index + 1});
+	}
+
+	template <size_t... I>
+	std::tuple<decltype(std::declval<Motions>().value())...> value(std::index_sequence<I...>)
+	{
+		return {std::get<I>(movements).value()...};
+	}
+
+	std::tuple<decltype(std::declval<Motions>().value())...> value()
+	{
+		return value(std::make_index_sequence<sizeof...(Motions)>{});
+	}
+
+	void reset()
+	{
+		for_all([](auto& movement) {movement.reset();});
+		current_index = 0;
+	}
+
+	template <typename T>
+	advance_result move(T& target, Program::duration delta)
+	{
+		auto result = advance(delta);
+		support::apply_for(current_index, [&target](auto&& movement)
+		{
+			target = movement.value();
+		}, movements);
+		return result;
+	}
+
+	template <typename... T,
+		 std::enable_if_t<sizeof...(T) == sizeof...(Motions)>* = nullptr>
+	advance_result move(std::tuple<T...>&& targets, Program::duration delta)
+	{
+		auto r = advance(delta);
+		support::apply_for(r.updated, [](auto&& movement, auto&& target)
+		{
+			target = movement.value();
+		}, movements, std::forward<std::tuple<T...>>(targets));
+		return r;
+	}
+};
+
+#endif /* end of include guard */
diff --git a/common/sketchbook.hpp b/common/sketchbook.hpp
index 4adb4a409fb67873249f5f025fa526e66bdbca55..bdcdb0a72547acd7eb3c439c392b986aa0702208 100644
--- a/common/sketchbook.hpp
+++ b/common/sketchbook.hpp
@@ -1,6 +1,7 @@
 #include <cstring>
 #include <cstdio>
 #include <thread>
+#include <mutex>
 #include <functional>
 #include <iostream>
 #include <random>
@@ -9,11 +10,13 @@
 
 #include "simple/support.hpp"
 #include "simple/graphical.hpp"
+#include "simple/musical.hpp"
 #include "simple/interactive/initializer.h"
 #include "simple/interactive/event.h"
 
 #include "simple_vg.h"
 #include "simple_vg.cpp"
+#include "math.hpp"
 
 #if defined __EMSCRIPTEN__
 #include <emscripten.h>
@@ -39,8 +42,7 @@ using rgba32 = graphical::rgba_pixel;
 using scancode = interactive::scancode;
 using keycode = interactive::keycode;
 using mouse_button = interactive::mouse_button;
-
-using std::vector;
+using common::lerp;
 
 constexpr int max_int = std::numeric_limits<int>::max();
 
@@ -57,13 +59,6 @@ auto trand_float(decltype(tiny_float_dist)::param_type range = {0, 1})
 auto trand_float2()
 { return float2(trand_float(), trand_float()); };
 
-template <typename Number, typename Ratio = float>
-constexpr
-Number lerp(Number from, Number to, Ratio ratio)
-{
-	return from + (to - from) * ratio;
-}
-
 template <size_t FPS>
 class framerate
 {
@@ -91,6 +86,16 @@ class Program
 	using mouse_move_fun = std::function<void(float2, float2)>;
 	using mouse_button_fun = std::function<void(float2, mouse_button)>;
 
+	using wave_fun = std::function<float(float ratio)>;
+	struct wave
+	{
+		wave_fun function;
+		duration total;
+		duration remaining;
+		explicit operator bool() { return bool(function); }
+	};
+	std::array<wave, 32> waves = {};
+	std::mutex dam;
 
 	bool run = true;
 	Program(const int argc, const char * const * const argv) : argc(argc), argv(argv) {}
@@ -115,10 +120,48 @@ class Program
 
 	bool running() { return run; }
 	void end() { run = false; }
+
+	auto request_wave(const wave& w, size_t canal)
+	{
+		return request_wave(wave{w}, canal);
+	}
+
+	bool request_wave(wave&& wave, size_t canal)
+	{
+		std::scoped_lock lock(dam);
+
+		assert(canal < waves.size());
+
+		auto& current = waves[canal];
+
+		if(current && current.remaining != 0s)
+		{
+			return false; // { false,  waves[canal].remaining }
+		}
+		current = std::move(wave);
+		current.remaining = current.total;
+		return true;
+	}
+
+	void require_wave(wave wave, size_t canal)
+	{
+		std::scoped_lock lock(dam);
+
+		assert(canal < waves.size());
+		auto& current = waves[canal];
+		current = std::move(wave);
+		current.remaining = current.total;
+	}
+
+
+	friend int main(int argc, char const* argv[]);
 };
 
 inline void process_events(Program&);
 
+// TODO: move dependent stuff out of here and include this at the top
+#include "motion.hpp"
+
 void start(Program&);
 
 int main(int argc, char const* argv[]) try
@@ -129,6 +172,44 @@ int main(int argc, char const* argv[]) try
 	graphical::initializer graphics;
 	sdlcore::initializer interactions(sdlcore::system_flag::event);
 
+	// TODO: make optional
+	musical::initializer music;
+	using namespace musical;
+	device_with_callback ocean
+	(
+		basic_device_parameters{spec{}
+			.set_channels(spec::channels::mono)
+			.set_format(format::int8)
+		},
+		[&program](auto& device, auto buffer)
+		{
+			std::fill(buffer.begin(), buffer.end(), device.silence());
+
+			auto lock = std::scoped_lock(program.dam);
+			const float tick = 1.f/device.obtained().get_frequency();
+			for(auto&& wave : program.waves)
+			{
+				if(wave && wave.remaining.count() >= tick)
+				{
+					float remaining = wave.remaining.count() / wave.total.count();
+					float progress = 1.f - remaining;
+					int remaining_ticks = std::min(buffer.size, int(wave.remaining.count() / tick));
+					auto step = tick/wave.total.count();
+					std::transform(buffer.begin(), buffer.begin() + remaining_ticks, buffer.begin(), [&](auto v)
+					{
+
+						progress += step;
+						return v + (wave.function(progress) * 127);
+					});
+					wave.remaining = (1.f - progress) * wave.total;
+					if(wave.remaining.count() < tick)
+						wave.remaining = 0s;
+				}
+			}
+		}
+	);
+	ocean.play();
+
 	gl_window::global.require<gl_window::attribute::major_version>(2);
 	gl_window::global.request<gl_window::attribute::stencil>(8);
 	gl_window win(program.name, program.size, gl_window::flags::borderless);
diff --git a/tools/setup/init.sh b/tools/setup/init.sh
index 7416ae516549aaa87ca91b3a69b5e155410dfc3c..e4dd73f22ab03ed12d5cf90489000b88f0786eb5 100755
--- a/tools/setup/init.sh
+++ b/tools/setup/init.sh
@@ -9,6 +9,7 @@ git clone https://notabug.org/namark/libsimple_geom
 git clone https://notabug.org/namark/libsimple_sdlcore
 git clone https://notabug.org/namark/libsimple_graphical
 git clone https://notabug.org/namark/libsimple_interactive
+git clone https://notabug.org/namark/libsimple_musical
 git clone https://notabug.org/namark/nanovg
 cd ..
 ./tools/setup/install.sh "$@"
diff --git a/tools/setup/install.sh b/tools/setup/install.sh
index 6d603749181a51d5796d84e6d9e8afc48dd81f27..28b2a397065297a9e4aecd29dbde68566d5646ff 100755
--- a/tools/setup/install.sh
+++ b/tools/setup/install.sh
@@ -7,6 +7,7 @@ make PREFIX=../libsimple_geom ../libsimple_geom/include/make_templates/header_on
 make PREFIX=../libsimple_sdlcore ../libsimple_sdlcore/include/make_templates/static_lib
 make PREFIX=../libsimple_graphical ../libsimple_graphical/include/make_templates/static_lib
 make PREFIX=../libsimple_interactive ../libsimple_interactive/include/make_templates/static_lib
+make PREFIX=../libsimple_musical ../libsimple_interactive/include/make_templates/static_lib
 make PREFIX=../nanovg ../nanovg/include/make_templates/static_lib
 cd ..
 
@@ -15,6 +16,7 @@ make install PREFIX=../../  "$@"
 make install PREFIX=../libsimple_sdlcore "$@"
 make install PREFIX=../libsimple_graphical "$@"
 make install PREFIX=../libsimple_interactive "$@"
+make install PREFIX=../libsimple_musical "$@"
 cd ..
 
 cd libsimple_geom
@@ -22,12 +24,14 @@ make install PREFIX=../../  "$@"
 make install PREFIX=../libsimple_sdlcore "$@"
 make install PREFIX=../libsimple_graphical "$@"
 make install PREFIX=../libsimple_interactive "$@"
+make install PREFIX=../libsimple_musical "$@"
 cd ..
 
 cd libsimple_sdlcore
 make install PREFIX=../../ "$@"
 make install PREFIX=../libsimple_graphical "$@"
 make install PREFIX=../libsimple_interactive "$@"
+make install PREFIX=../libsimple_musical "$@"
 cd ..
 
 cd libsimple_graphical
@@ -38,6 +42,10 @@ cd libsimple_interactive
 make install PREFIX=../../ "$@"
 cd ..
 
+cd libsimple_musical
+make install PREFIX=../../ "$@"
+cd ..
+
 cd nanovg
 make install GL=2 PREFIX=../../ "$@"
 cd ..
diff --git a/tools/setup/update.sh b/tools/setup/update.sh
index 91c5574097737a1b6f3bce6ffc68a578db6af112..c5d368c56f9ad263c179a3e9a1a6424e3c1004d1 100755
--- a/tools/setup/update.sh
+++ b/tools/setup/update.sh
@@ -26,6 +26,10 @@ cd libsimple_interactive
 git pull -r
 cd ..
 
+cd libsimple_musical
+git pull -r
+cd ..
+
 cd nanovg
 git pull -r
 cd ..