C++로 프론트엔드 건드리기

2019-12-20, Fri

Emscripten을 이용하면 C/C++을 JS로 컴파일할 수 있다.

JS와 DOM HTML을 다 알아야 짤 수는 있지만(ㅋㅋ) 일단 JS 자체를 사용하진 않았다.




WebAudio로 FFT하기 여기 있는 첫번째 예제를 따라해본건데,

<select id='waveform'>
<option value='sine'>Sine</option>
<option value='square'>Square</option>
<option value='sawtooth'>Sawtooth</option>
<option value='triangle'>Triangle</option>
</select>
<input type="number" id="freq" value="440">
<button class="cppAudioButton" id="pl">Play</button>
<button class="cppAudioButton" id="st" style="display:none">Stop</button>

<style>
.cppAudioButton {
margin: 10px;
padding: 10px;
border-radius: 10px;
font-size: 20px;
}
</style>

<script src='a.out.js'></script>

이런 식으로 기본적인 틀을 잡아주고

#include <iostream>

#include <emscripten/bind.h>
#include <emscripten/val.h>

using emscripten::val;
using namespace std::literals;

bool hasStarted  = false;
val AudioContext = val::null(), osc = val::null();

val html_element(const std::string& elemSelector)
{
	return val::global("document").call<val>("querySelector", elemSelector);
}

void setType(const val& = val::null())
{
	osc.set("type", html_element("#waveform")["value"]);
}

void setFreq(const val& = val::null())
{
	osc["frequency"].call<void>("setValueAtTime", val::global("window").call<int>("parseInt", html_element("#freq")["value"]), AudioContext["currentTime"]);
}

void toggle()
{
	val v       = val::global("document").call<val>("querySelectorAll", ".cppAudioButton"s);
	auto querys = emscripten::vecFromJSArray<val>(v);
	for (auto elem : querys)
	{
		if (val::global("window").call<val>("getComputedStyle", elem)["display"].as<std::string>() == "none")
			elem["style"].set("display", "block");
		else
			elem["style"].set("display", "none");
	}
}

void play(const val& = val::null())
{
	setFreq();
	setType();

	osc.call<void>("connect", AudioContext["destination"]);
	if (!hasStarted)
	{
		osc.call<void>("start", 0);
		hasStarted = true;
	}
	toggle();
}

void stop(const val& = val::null())
{
	osc.call<void>("disconnect");
	toggle();
}

int main()
{
	val AudioCtx = val::global("AudioContext");
	if (!AudioCtx.as<bool>())
	{
		std::cout << "No global AudioContext, trying webkitAudioContext\n";
		AudioCtx = val::global("webkitAudioContext");
	}

	std::cout << "Got an AudioContext\n";
	AudioContext = AudioCtx.new_();
	osc	  = AudioContext.call<val>("createOscillator");

	html_element("#pl").set("onclick", val::module_property("play"));
	html_element("#st").set("onclick", val::module_property("stop"));
	html_element("#waveform").set("onchange", val::module_property("setType"));
	html_element("#freq").set("onchange", val::module_property("setFreq"));
}

EMSCRIPTEN_BINDINGS(my_module)
{
	emscripten::function("play", play);
	emscripten::function("stop", stop);
	emscripten::function("setType", setType);
	emscripten::function("setFreq", setFreq);
}

이렇게 구현한다. 위의 링크와 emscripten 레퍼런스를 보고 구현해본 건데, emscripten을 처음 쓰는 것도 아닌데도 한 6시간 붙잡고 있었다.
콜백에 람다를 넘기고 싶은데 그게 안돼서 한시간 정도 헤매다가 전역함수로 깔고, 상태도 어찌 해야 하나 하다가 전역변수로 전부 깔았다. 그 와중에 emscripten::val은 기본 생성자가 delete되어 있어서 null로 초기화해줘야 된다. 이유는 모르겠으나 구현 마음대로겠지 뭐 ㅋㅋ

규모가 커지면 이렇게 구현이 안되겠지만 그건 그때 가서 디자인 패턴으로 만져야 할 것이기에 일단은 이렇게만 한다..