Something went wrong. Try again.
A proof-of-concept proposal for turning standard Rubik's Cubes into smartcubes by embedding speakers into the cube's centercaps.
Something went wrong. Try again.
74 kB · 813 lines
at commit e2a52c65
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# This Source Code Form is subject to the terms of the Mozilla Public\n", "# License, v. 2.0. If a copy of the MPL was not distributed with this\n", "# file, You can obtain one at https://mozilla.org/MPL/2.0/." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# The Receiver" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Introduction\n", "The receiver for the sound-based move tracking protocol is in charge of decoding a sequence of face turns from the transmitted audio.\n", "\n", "To do this, the receiver must go through the following steps:\n", "- Record the transmitted audio.\n", "- Measure the audible frequencies at each time step.\n", "- Convert each time step's audible frequencies to the cube's state at that moment.\n", "- Decode the applied face turns from the sequence of cube states.\n", "\n", "\n", "This chapter will describe the development of a software algorithm in Python that can serve as a receiver for this sound-based move tracking protocol. \n", "This development began with the creation of synthetic audio recordings representing the tones that would be emitted by a Rubik's Cube equipped with an ideal transmitter (Section \\ref{sec:synthetic-audio-generation}) followed by the implementation of an algorithm capable of decoding that ideal synthetic audio (Section \\ref{sec:decoding-synthetic-audio}).\n", "The algorithm was then made more robust by adding realistic noise to the synthetic audio to better simulate a real speedcubing environment (Section \\ref{sec:adding-realistic-noise}) followed by enhancing the previously designed algorithm to continue to decode the applied move sequence in the midst of the added noise (Section: \\ref{sec:decoding-realistic-noise})." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Synthetic Audio Generation\n", "The first step of designing a receiver is to synthesize an audio signal representative of the output of the ideal receiver.\n", "\n", "This is done by encoding the frequency corresponding to each centerpiece state (Section \\ref{subsec:represent-audio-protocol}), creating a virtual Rubik's Cube (Section \\ref{subsec:represent-rubiks-cube}), and finally generating the audio signal from the virtual Rubik's Cube's state (Section \\ref{subsec:generate-audible-algorithm})." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Representing the Audio Protocol\n", "For the synthetic audio generator to produce a realistic signal, it needs to know which frequencies to transmit for each centerpiece state. \n", "For this design the frequencies listed in Table \\ref{table:centerpiece-frequencies} are converted to a dictionary." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [], "source": [ "CENTERPIECE_ROTATION_TO_FREQUENCY_MAPPINGS = {\n", " \"U\": {\n", " 0: 800,\n", " 1: 900,\n", " 2: 1000,\n", " 3: 1100,\n", " },\n", " \"D\": {\n", " 0: 1300,\n", " 1: 1400,\n", " 2: 1500,\n", " 3: 1600,\n", " },\n", " \"R\": {\n", " 0: 1800,\n", " 1: 1900,\n", " 2: 2000,\n", " 3: 2100,\n", " },\n", " \"L\": {\n", " 0: 2300,\n", " 1: 2400,\n", " 2: 2500,\n", " 3: 2600,\n", " },\n", " \"F\": {\n", " 0: 2800,\n", " 1: 2900,\n", " 2: 3000,\n", " 3: 3100,\n", " },\n", " \"B\": {\n", " 0: 3300,\n", " 1: 3400,\n", " 2: 3500,\n", " 3: 3600,\n", " }\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "With this dictionary in place, determining the frequency to transmit for any particular centerpiece's current rotation is reduced to a simple lookup." ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "def frequency_of(centerpiece: str, rotation: int) -> float:\n", " return CENTERPIECE_ROTATION_TO_FREQUENCY_MAPPINGS[centerpiece][rotation]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Representing the Rubik's Cube\n", "Next, the synthetic audio generator needs a representation of a Rubik's Cube on which face turns can be virtually applied, and the resulting cube state can be read out." ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "class RubiksCube:\n", " \n", " CLOCKWISE = 1\n", " COUNTERCLOCKWISE = 3\n", "\n", " def __init__(self):\n", " self.state = { \"U\": 0, \"D\": 0, \"R\": 0, \"L\": 0, \"F\": 0, \"B\": 0 }\n", " \n", " def apply_move(self, move: str):\n", " # Extract the face and direction from the move string.\n", " face = move[0]\n", " if len(move) == 1: # e.g. U\n", " direction = RubiksCube.CLOCKWISE\n", " else: # e.g. U'\n", " direction = RubiksCube.COUNTERCLOCKWISE\n", " # Update the state to apply the move.\n", " self.state[face] = (self.state[face] + direction) % 4" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Generating Synthetic Audio for an Arbitrary Algorithm\n", "Now the synthetic audio can be generated for any valid algorithm.\n", "This is done using the \"tones\" library \\cite{pip-tones} created by Erik Nyquist.\n", "First, a separate audio track is created for each centerpiece on the cube and the frequencies representing the initial state of each centerpiece are added to its corresponding audio track.\n", "From there, the rest of the synthetic audio can be created by iterating through each step of the given algorithm, applying it to the virtual Rubik's Cube, and adding the frequency for each centerpiece's resulting state to its corresponding audio track." ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [], "source": [ "from tones.mixer import Mixer # https://pypi.org/project/tones/\n", "from tones import SINE_WAVE\n", " \n", "def _create_mixer(rubiks_cube: RubiksCube) -> Mixer:\n", " mixer: Mixer = Mixer(sample_rate=44100, amplitude=1)\n", " # Add a separate track for each centerpiece.\n", " for face, _ in rubiks_cube.state.items():\n", " mixer.create_track(face, SINE_WAVE, attack=0, decay=0)\n", " return mixer\n", "\n", "def _render_cube_state(mixer: Mixer, rubiks_cube: RubiksCube, tps: float):\n", " for face, rotation in rubiks_cube.state.items():\n", " mixer.add_tone(face, frequency_of(face, rotation), duration=1 / tps)\n", "\n", "def render_audible_alg(alg: str, wav_path: str=None, tps: float=4):\n", " # Create the virtual Rubik's Cube.\n", " rubiks_cube: RubiksCube = RubiksCube()\n", " \n", " # Create the audio mixer used to create the synthesized audio.\n", " mixer = _create_mixer(rubiks_cube)\n", " \n", " # Add the initial cube state to the mixer.\n", " _render_cube_state(mixer, rubiks_cube, tps)\n", " \n", " # Iterate over the moves in the algorithm, adding\n", " # the cube state to the mixer after each move.\n", " moves = alg.split(\" \")\n", " for move in moves:\n", " rubiks_cube.apply_move(move)\n", " _render_cube_state(mixer, rubiks_cube, tps)\n", " \n", " # Save the final audio to a .wav file.\n", " mixer.write_wav(wav_path if wav_path else f\"{alg}.wav\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "With these functions in place, synthetic audio can be easily created for any valid Rubik's Cube algorithm. For example, generating synthetic audio that sweeps through every possible centerpiece state can be done with the following snippet of code:" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [], "source": [ "demo_alg = \"U U U U D D D D R R R R L L L L F F F F B B B B\"\n", "demo_wav_path = \"demo_all_states.wav\"\n", "render_audible_alg(demo_alg, demo_wav_path)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Decoding Move Sequences from Synthetic Audio\n", "With synthetic audio now available for any valid Rubik's Cube algorithm, the next step is to create an initial software algorithm that can decode that audio back into the original move sequence.\n", "\n", "TODO summarize the subsections that make this possible." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Compute the Spectrogram\n", "The first step in decoding the synthetic audio is determining its component frequencies at any specific moment in time.\n", "These component frequencies can be easily visualized using a spectrogram, like the one in Figure \\ref{fig:spectrogram} of the synthetic audio created in Section \\ref{subsec:generate-audible-algorithm}." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The bright, horizontal yellow bands in the spectrogram represent the component frequencies present in the audio signal at each point in time.\n", "The vertical red line on the spectrogram indicates the specific slice of the spectrogram represented by the graph of the component frequencies at that specific point in time.\n", "\n", "While the Figure \\ref{fig:spectrogram} was created using Matplotlib's specgram plot, the actual data of the spectogram for a given .wav file can be easily obtained using the numpy and scipy packages" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "from scipy import signal\n", "from scipy.io import wavfile\n", "\n", "def compute_spectrogram(wav_path: str):\n", " SAMPLES_PER_WINDOW = 1024 # Balances frequency/time precision\n", " sample_rate, audio_samples = wavfile.read(wav_path)\n", " freq, time, Zxx = signal.stft(audio_samples, fs=sample_rate,\n", " nperseg=SAMPLES_PER_WINDOW, noverlap=(SAMPLES_PER_WINDOW // 4) * 3)\n", " spectrogram = np.abs(Zxx).transpose()\n", " return freq, time, spectrogram" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Visualization" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [], "source": [ "# Heavily aided by https://matplotlib.org/stable/gallery/animation/simple_anim.html#sphx-glr-gallery-animation-simple-anim-py\n", "import matplotlib.pyplot as plt\n", "import matplotlib.animation as animation\n", "import numpy as np\n", "from scipy import signal\n", "from scipy.io import wavfile\n", "\n", "def compute_threshold(values: list):\n", " return -500 # Out of sight of the chart\n", "\n", "def animate(wav_path: str, alg: str):\n", " # Parse the audio sample into its spectrogram\n", " SAMPLES_PER_WINDOW = 1024 # Seems to balance frequency precision with time precision.\n", " sample_rate, audio_samples = wavfile.read(wav_path)\n", " freq, time, Zxx = signal.stft(audio_samples, fs=sample_rate,\n", " nperseg=SAMPLES_PER_WINDOW, noverlap=(SAMPLES_PER_WINDOW // 4) * 3)\n", " spectrogram = np.abs(Zxx).transpose()\n", "\n", " # Generate the plots\n", " fig, (ax_spectrogram, ax_fourier) = plt.subplots(2)\n", "\n", " # Set the spectrogram\n", " ax_spectrogram.specgram(audio_samples, Fs=sample_rate, \n", " NFFT=SAMPLES_PER_WINDOW,\n", " noverlap=SAMPLES_PER_WINDOW // 4 * 3) # 3/4 of the window size\n", " line_spectrogram, = ax_spectrogram.plot([], [], '-', color='red')\n", " time_tracker_spectrogram = ax_spectrogram.axvline(0, color='red')\n", " ax_spectrogram.set_ylim(0, 5000)\n", " ax_spectrogram.set_title(f\"Spectrogram\")\n", " ax_spectrogram.set_ylabel('Frequency [Hz]')\n", " ax_spectrogram.set_xlabel('Time [sec]')\n", "\n", " # Set the Fourier transform\n", " STEP = 10\n", " indices = np.arange(0, len(time) + 1, step=STEP)\n", " line_fourier, = ax_fourier.plot(freq, spectrogram[0], color='red')\n", " threshold_fourier = ax_fourier.axhline(-500, color='green')\n", " ax_fourier.set_title(\"\")\n", " ax_fourier.set_xlim(0, 5000) # Frequency\n", " ax_fourier.set_xlabel(\"Frequency [Hz]\")\n", " ax_fourier.set_ylim(0, np.amax(spectrogram)) # Strength\n", " ax_fourier.set_ylabel(\"Strength\")\n", "\n", " # Space figures nicely\n", " fig.suptitle(alg)\n", " fig.tight_layout(h_pad=3)\n", "\n", " def draw_frame_at(i):\n", " time_tracker_spectrogram.set_xdata(time[i])\n", " line_fourier.set_ydata(spectrogram[i])\n", " threshold_fourier.set_ydata(compute_threshold(spectrogram[i]))\n", " ax_fourier.set_title(f\"Frequencies at {time[i]:.2f} seconds [idx={i}]\")\n", " return line_spectrogram, line_fourier\n", "\n", " ani = animation.FuncAnimation(fig, draw_frame_at,\n", " frames=indices, interval=1, blit=True, save_count=len(time)/STEP)\n", " ani.save(f\"plt/animation/{wav_path[:-4]}.mp4\",\n", " fps=len(time)/STEP/5, dpi=300)\n", " plt.show()\n" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "c:\\Users\\Joseph Hale\\git\\DIY-Supercube\\venv\\lib\\site-packages\\matplotlib\\axes\\_axes.py:7723: RuntimeWarning: divide by zero encountered in log10\n", " Z = 10. * np.log10(spec)\n" ] }, { "data": { "image/png": "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", "text/plain": [ "<Figure size 432x288 with 2 Axes>" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# animate(demo_wav_path, demo_alg)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Extract the Dominant Component Frequencies\n", "Notice how the component frequency graph in Figure \\ref{fig:spectrogram} has six peaks: these are the transmitted frequencies representing the current state of the virtual Rubik's Cube's six centerpieces at that specific instant of time.\n", "\n", "The exact frequencies of these peaks can be extracted by filtering out all frequencies not above a specific threshold.\n", "In this case, a simple threshold of 85\\% of the maximum strength of any component frequency (see the green line in Figure \\ref{fig:spectrogram-with-naive-threshold}) isolates the peak of the six dominant component frequencies." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Using the actual spectogram data we can compute the exact values of these peaks." ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['818 Hz', '1593 Hz', '1809 Hz', '2283 Hz', '2799 Hz', '3316 Hz']\n" ] } ], "source": [ "def compute_threshold(values: list):\n", " return max(values) * 0.85\n", "\n", "def extract_important_frequencies(freq, time, spectrogram, time_idx):\n", " important_freqs = []\n", " threshold = compute_threshold(spectrogram[time_idx])\n", " for freq_idx in range(len(freq)):\n", " if spectrogram[time_idx][freq_idx] > threshold:\n", " important_freqs.append(dict(\n", " hz=freq[freq_idx],\n", " power=spectrogram[time_idx][freq_idx]\n", " ))\n", " return important_freqs\n", "\n", "freq, time, spectrogram = compute_spectrogram(demo_wav_path)\n", "important_freqs = extract_important_frequencies(freq, time, spectrogram,\n", " time_idx=310) # 1.80 seconds\n", "\n", "important_freqs_hz = [i[\"hz\"] for i in important_freqs]\n", "pretty_freqs = list(map(lambda x: f\"{x:.0f} Hz\", important_freqs_hz))\n", "print(pretty_freqs)" ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [], "source": [ "# animate(demo_wav_path, demo_alg)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Translating Component Frequencies to Centerpiece States\n", "With the specific frequencies of each detected peak, the original state of the Rubik's Cube at that moment in time can be computed by finding the state whose corresponding frequency is closest to each detected peak frequency." ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'U': 0, 'D': 3, 'R': 0, 'L': 0, 'F': 0, 'B': 0}\n" ] } ], "source": [ "def _closest_state(detected_freq):\n", " closest_rotation = None\n", " closest_difference = None\n", " for face, rotations in CENTERPIECE_ROTATION_TO_FREQUENCY_MAPPINGS.items():\n", " for rotation, freq in rotations.items():\n", " difference = abs(detected_freq - freq)\n", " if closest_difference is None or difference < closest_difference:\n", " closest_difference = difference\n", " closest_rotation = dict(face=face, rotation=rotation)\n", " return closest_rotation\n", "\n", "def get_state_from_freqs(important_freqs: list) -> dict:\n", " state = {}\n", " for freq in important_freqs:\n", " hz, power = freq.values()\n", " closest_state = _closest_state(hz)\n", " state[closest_state['face']] = closest_state['rotation']\n", " return state\n", "\n", "print(get_state_from_freqs(important_freqs))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Repeating this process for each time step in the recorded audio will yield a sequence of the Rubik's Cube's centerpiece states over the course of the recorded audio sequence." ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [], "source": [ "def get_state_over_time(freq, time, spectrogram):\n", " state_over_time = []\n", " for time_idx in range(len(time)):\n", " important_freqs = extract_important_frequencies(\n", " freq, time, spectrogram, time_idx)\n", " state = get_state_from_freqs(important_freqs)\n", " state_over_time.append(dict(\n", " time=time[time_idx],\n", " state=state\n", " ))\n", " return state_over_time\n", "\n", "state_over_time = get_state_over_time(freq, time, spectrogram)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Extracting Move Sequences from Centerpiece State Sequences\n", "Finally, the original move sequence can be recovered by iterating over the sequence of cube states, and registering any change to the cube state as a move applied to the cube." ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "U U U U D D D D R R R R L L L L F F F F B B B B\n", "Matches demo_alg? True\n" ] } ], "source": [ "def _move_from(face, current_rotation, new_rotation):\n", " direction = None\n", " if (current_rotation + RubiksCube.CLOCKWISE) % 4 == new_rotation:\n", " direction = \"\" # Clockwise\n", " elif (current_rotation + RubiksCube.COUNTERCLOCKWISE) % 4 == new_rotation:\n", " direction = \"'\" # Counterclockwise\n", " return f\"{face}{direction}\"\n", "\n", "def detect_moves(state_over_time):\n", " detected_moves = []\n", " current_state = {}\n", " for idx, timed_state in enumerate(state_over_time):\n", " time, state = timed_state.values()\n", " for face, rotation in state.items():\n", " if not (face in current_state):\n", " current_state[face] = rotation\n", " if rotation != current_state[face]:\n", " detected_moves.append(dict(\n", " time=time,\n", " move=_move_from(face, current_state[face], rotation)\n", " ))\n", " current_state[face] = rotation\n", " return detected_moves\n", "\n", "detected_moves = detect_moves(state_over_time)\n", "\n", "pretty_moves = \" \".join([i[\"move\"] for i in detected_moves])\n", "print(pretty_moves) \n", "print(f\"Matches demo_alg? {demo_alg == pretty_moves}\") " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Clearly, the detected move sequence matches the demo algorithm used to create the synthetic audio in Section \\ref{subsec:generate-audible-algorithm}, which means this algorithm is a functional receiver for an ideal transmitter." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*Side Note: Here's the full implementation for a second example. This is provided here so it's easy to find a complete example for detecting a new algorithm even though it's not included in the main Thesis document.*" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "# demo2_alg = \"U U' D D' R R' L L' F F' B B'\"\n", "# demo2_wav_path = \"demo_all_turns.wav\"\n", "# render_audible_alg(demo2_alg, demo2_wav_path)\n", "\n", "# freq2, time2, spectrogram2 = compute_spectrogram(demo2_wav_path)\n", "# state_over_time2 = get_state_over_time(freq2, time2, spectrogram2)\n", "# detected_moves2 = detect_moves(state_over_time2)\n", "\n", "# pretty_moves2 = \" \".join([i[\"move\"] for i in detected_moves2])\n", "# print(pretty_moves2)\n", "# print(f\"Matches demo2_alg? {demo2_alg == pretty_moves2}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Adding Realistic Noise to Synthetic Audio" ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [], "source": [ "# NOT FOR INCLUSION IN THE THESIS, LEGACY MAPPINGS USED IN TRANSMITTED SIGNAL\n", "CENTERPIECE_ROTATION_TO_FREQUENCY_MAPPINGS = {\n", "\t\"U\": {\n", "\t\t0: 799.872020476724,\n", "\t\t1: 899.604174163368,\n", "\t\t2: 1000.40016006403,\n", "\t\t3: 1100.5943209333\n", "\t},\n", "\t\"D\": {\n", "\t\t0: 1250.93820365274,\n", "\t\t1: 1350.25654874426,\n", "\t\t2: 1448.85540423066,\n", "\t\t3: 1549.42671211652,\n", "\t},\n", "\t\"R\": {\n", "\t\t0: 1700.68027210884,\n", "\t\t1: 1799.20834832674,\n", "\t\t2: 1899.69604863222,\n", "\t\t3: 2000.80032012805,\n", "\t},\n", "\t\"L\": {\n", "\t\t0: 2296.73863114378,\n", "\t\t1: 2497.5024975025,\n", "\t\t2: 2695.41778975741,\n", "\t\t3: 2903.60046457607,\n", "\t},\n", "\t\"F\": {\n", "\t\t0: 3203.07495195388,\n", "\t\t1: 3401.36054421769,\n", "\t\t2: 3607.50360750361,\n", "\t\t3: 3799.39209726444,\n", "\t},\n", "\t\"B\": {\n", "\t\t0: 4105.09031198686,\n", "\t\t1: 4302.92598967298,\n", "\t\t2: 4492.36298292902,\n", "\t\t3: 4699.24812030075\n", "\t}\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Decoding the Realistic Audio" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Fine-Tuning the Threshold" ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [], "source": [ "def compute_threshold(values: list, stdv_pct: float=1, min_thresh: int=50):\n", " return max(min_thresh, np.std(values) * stdv_pct)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Filtering through similar peak frequencies" ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'U': 0, 'D': 0, 'R': 0, 'L': 0, 'F': 0, 'B': 0}\n" ] } ], "source": [ "def get_state_from_freqs(important_freqs: list) -> dict:\n", " state = {}\n", " state_power = {\"U\": 0, \"D\": 0, \"R\": 0, \"L\": 0, \"F\": 0, \"B\": 0} # New\n", " for freq in important_freqs:\n", " hz, power = freq.values()\n", " closest_state = _closest_state(hz)\n", " if power > state_power[closest_state[\"face\"]]: # New\n", " state[closest_state[\"face\"]] = closest_state[\"rotation\"]\n", " state_power[closest_state[\"face\"]] = power # New\n", " return state\n", "\n", "transmitted_wav_path = \"transmitted-356-5tps.wav\"\n", "freq, time, spectrogram = compute_spectrogram(transmitted_wav_path)\n", "important_freqs = extract_important_frequencies(freq, time, spectrogram,\n", " time_idx=800) # 4.64 seconds\n", "print(get_state_from_freqs(important_freqs))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Ignoring Noise when Extracting Move Sequences" ] }, { "cell_type": "code", "execution_count": 51, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "U U U U D D D D R R R R L L L L F F F F B B B B\n", "Matches demo_alg? True\n" ] } ], "source": [ "def detect_moves(state_over_time, window_size=8): # Edit\n", " detected_moves = []\n", " current_state = {}\n", " new_state = {\"U\": -1, \"D\": -1, \"R\": -1, \"L\": -1, \"F\": -1, \"B\": -1} # New\n", " new_state_idx = {\"U\": 0, \"D\": 0, \"R\": 0, \"L\": 0, \"F\": 0, \"B\": 0} # New\n", " for idx, timed_state in enumerate(state_over_time):\n", " time, state = timed_state.values()\n", " for face, rotation in state.items():\n", " # Update new_state whenever the rotation changes\n", " if rotation != new_state[face]:\n", " new_state[face] = rotation\n", " new_state_idx[face] = idx\n", " # If the new state has persisted over window_size time steps...\n", " if idx - new_state_idx[face] == window_size:\n", " if not (face in current_state):\n", " current_state[face] = new_state[face]\n", " # ... and it's a different rotation than the current state...\n", " elif new_state[face] != current_state[face]:\n", " # ... then we have detected a face turn!\n", " detected_moves.append(dict(\n", " time=state_over_time[new_state_idx[face]][\"time\"],\n", " move=_move_from(face, current_state[face], rotation)\n", " ))\n", " current_state[face] = rotation\n", " new_state_idx[face] = 0\n", " return detected_moves\n", "\n", "state_over_time = get_state_over_time(freq, time, spectrogram)\n", "detected_moves = detect_moves(state_over_time)\n", "\n", "pretty_moves = \" \".join([i[\"move\"] for i in detected_moves])\n", "print(pretty_moves) \n", "print(f\"Matches demo_alg? {demo_alg == pretty_moves}\")" ] } ], "metadata": { "interpreter": { "hash": "b9bb1448f44d41b50ef724d977467d677366b56f1ac5b7ddcacc005f02a40cb4" }, "kernelspec": { "display_name": "Python 3.9.1 64-bit ('venv': venv)", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.1" }, "orig_nbformat": 4 }, "nbformat": 4, "nbformat_minor": 2}