Misja Gynvaela 009

argeento -
  • #javascript
  • #audio
  • #writeup

Od czasu do czasu na kanale Gynvaela pojawiają się zadania, polegające na odszyfrowaniu zmyślnie ukrytego hasła. Tym razem na warsztat pójdzie tajemnicza wiadomość dzwiękowa.

MISJA 009          goo.gl/q49Fw7               DIFFICULTY: ██████       [6/10]
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
Do naszych techników trafiło nagranie, w postaci pliku dźwiękowego,
z osobliwymi piskami. Nagranie otrzymaliśmy od lokalnego radioamatora
i możesz je pobrać poniżej:

  https://goo.gl/NeJHD2

Jeśli możesz, wyręcz naszych techników w zdekodowaniu wiadomości - są obecnie
zajęci naprawą naszego elektrohydroturbobulbulatora.

Powodzenia!

Z bulbulatorami nie ma żartów, trzeba brać się do pracy. Nagranie od radioamatora:

Słychać wyraźnie dwa tony, które następują po sobie w losowy sposób. Obserwując czas odtwarzania, można zauważyć, że każdy dźwięk trwa dokładnie sekundę. To cenna wskazówka, która znacznie ułatwi odszyfrowanie.

W przeciągu 136 sekund mamy zatem zestaw 136 danych w postaci

['ton niższy', 'ton wyższy', 'ton wyższy', ...]

Wiadomość bitowa! W dodatku liczba bitów jest podzielna przez 8. Teraz tylko wpisać wszystkie dane w tablicę. Co się okazuję, wystarczy odrobina skupienia, aby bez problemu uzupełnić jedynki i zera ze słuchu. W tym przypadku to zdecydowanie najszybszy sposób.

const bits = '011000100100111010100110100011101010111010100110011101...'

A co jeśli nagranie trawło by 3 godziny?

Analiza sygnału

W npm do wszystkiego znajdzie się odpowiednia biblioteka
(WebAudioLoader + lodash + webpack + file-loader)

import WebAudioLoader from 'webaudioloader'
import wavFile from './nagranie.wav'
import { chunk } from 'lodash'

const wal = new WebAudioLoader({
    context: new AudioContext()
})

wal.load(wavFile, {
    onload(err, buffer) {

        console.log(buffer)
        // Informacje o sygnale:
        //  - liczba kanałów: 1
        //  - liczba próbek: 5997600
        //  - częstotliwość próbkowania: 44100
        //  - czas trwania: 136

        const samples = buffer.getChannelData(0)
        // w tym miejscu można przejśc do analizy próbek
    }
})

Na każdy dzwięk przypada więc 44100 próbek, które pogrupujemy w 136 paczek.

const samplePacks = chunk(data, buffer.sampleRate)

Do każdej paczki trzeba przyporządkować 1 albo 0 znajdując dowolną matematyczną zależność.

Dla przykładu: ile próbek w paczce jest mniejszych od 0.2?

samplePacks.forEach((samplePack, index) => {
    const lessThan02 = number => number < 0.2
    console.log(index, samplePack.filter(lessThan02).length)
})

// Próbka 1: 28126
// Próbka 2: 28224
// Próbka 3: 28224
// Próbka 4: 28126
// Próbka 5: 28126
// ...

Występują dwie liczby: 28 126, 28 224. Na ich podstawie można przypisać paczkom odpowiednie wartości.

const bits = samplePacks.map(samplePack => {
    const lessThan02 = number => number < 0.2
    return samplePack.filter(lessThan02).length === 28126 ? 0 : 1

    // Drugą możliwością byłoby ? 1 : 0
    // jednak taka konfiguracja nie przynosi żadnych sensownych rezultatów
})

W tym momencie niestraszne nawet n-godzinne nagrania.

Rozszyfrowanie wiadomości

Pogrupowanie bitów w bajty

const bytes = chunk(bits, 8)
01100010
01001110
10100110
10001110
10101110
10100110
01110110
11000110
10011110
00000100
11101010
10000110
01001110
01001110
10010110
11110110
01001110

W oczy rzuca się ostatnia kolumna zer - niepotrzebne bity w oryginalnym 7-bitowym ASCII - kolejnośc bitów w bajtach jest odwrócona.

const reverseBytes = byte => byte.reverse()
const bytes = chunk(bits, 8).map(reverseBytes)

Konwersja bajtów na liczby w systemie dziesiętnym

const parsByteToInt = byte => parseInt(byte.join(''), 2)
const ints = bytes.map(parseArrayToInt)

Odczytanie wiadomości

const message = String.fromCharCode(...ints)
console.log(message) // "Frequency Warrior"

Wizualizacja

Mając wszystkie dane można narysować prosty wykres

<canvas class="decode" width="1400" height="200"></canvas>
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext('2d')
ctx.fillStyle = '#6e90f3'
for (let i = 0; i < data.length; i += 5) {
    ctx.fillRect(i / 2500, 100 + data[i] * 180, 0.1, 0.1)
}

Cały projekt udostępniony jest na githubie