Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
RandomUsername96 committed Oct 1, 2016
0 parents commit d8a21f2
Show file tree
Hide file tree
Showing 12 changed files with 530 additions and 0 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# such-wav-very-wow

**Design doc**:
https://docs.google.com/document/d/1wTqI9ibEF-vHbr2aUqO6_ZJvHAKXhTdXMFTS0o6OocA/edit

**img_convert.py** - pomoćna skripta koja konvertuje slike u b&w mod
**audiolib.py** - pomoćna biblioteka

**morse.py** - morse code enkoder/dekoder za prvi deo zadatka
**bfsk.py** - bfsk ascii enkoder/dekoder za drugi deo zadatka
**sstv.py** - mrrobot b&w n8 SSTV enkoder/dekoder za treći deo zadatka

**gen_tekst.sh** - generiše tekst zadatka u tekst.wav (morse) i odmah ga dekodira radi provere
**gen_2.sh** - generiše 2.wav (bfsk) i odmah dekodira radi provere
**gen_3.sh** - generiše 3.wav (sstv) i odmah dekodira radi provere

**qr.jpg** - ulaz za treći deo zadatka
**qr_sol.jpg** - izlaz sstv.py nakon encode+decode

**such-wav-very-wow.zip** - Zip arhiva (da, zip u git repou, stvarno) koja sadrži {tekst.wav, 2.wav, 3.wav} tj. sve što treba da bude shipovano kao zadatak pored naslova.
76 changes: 76 additions & 0 deletions audiolib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""
Osnovne audio funkcije i promenljive koje su manje vise potrebne u svakom fajlu.
Dodate jos neke funkcije koje nisu striktno u vezi sa zvukom ali su potrebne na
vise mesta. Pravljenje jos jedne biblioteke zbog dve male fje bi bilo previse bahato.
"""
import math
import struct
import wave

# global consts, no real reason to ever change these
nchannels = 1 # number of channels
bitdepth = 2 # bits per sample
framerate = 44100.0 # samples/frames per second
comptype = "NONE" # compression type
compname = "not compressed" # compression name
amp_range = 64000.0 # multiplier for amplitude, amplitude range

# splits list L into chunks of size sz and returns a list of lists
def chunks(L, sz):
return [L[start:start+sz] for start in range(0, len(L), sz)]

# usual flatten operation, converts nested list to a regular list
def flatten(L):
return [elem for inner in L for elem in inner]

# helper function that converts length in ms to frames
def s2f(ms):
return round(framerate * ms / 1000)

# noob approach to extracting frequencies from an array of frames
def extract_freqs_noob(frames):
# monitors every time waveform crosses X axis while going upwards
# and calculates frequencies for each period
freqs_frames = [] # (frequency, number of frames) tuple
last_frame = curr_num_frames = 0
for frame in frames:
if last_frame < 0 and frame >= 0:
# one period is over
freqs_frames.append((round(framerate / curr_num_frames), curr_num_frames))
curr_num_frames = 0
curr_num_frames += 1
last_frame = frame
return freqs_frames

# uses provided starting phase and frequency to generate num_frames audio frames
# returns a tuple (list of frames, phase for the next part of the waveform)
def create_frames(freq, num_frames, phase):
ret = []
delta = 0 if freq == 0 else (framerate / freq) * (phase / (2 * math.pi))
for x in range(num_frames):
phase = (2*math.pi*freq*((x+delta)/framerate)) % (2*math.pi)
ret.append(math.sin(phase)) # [-1, 1], will be multiplied by amp
out_phase = (2*math.pi*freq*((num_frames+delta)/framerate)) % (2*math.pi)
return (ret, out_phase)

# writes input frames to the specified wav file
def write_wav(frames, wav_filename):
# opens output file and sets params
wav_file = wave.open(wav_filename, "w")
wav_file.setparams((nchannels, bitdepth, int(framerate), len(frames),
comptype, compname))

# writes audio frames to file and closes it afterwards
for frame in frames:
wav_file.writeframes(struct.pack('h', int(frame*amp_range/2)))
wav_file.close()

# returns frames from the specified wav file
def read_wav(wav_filename):
wav_file = wave.open(wav_filename, "r")
nframes = wav_file.getnframes()
if wav_file.getparams() != (nchannels, bitdepth, int(framerate), nframes, comptype, compname):
raise Exception('input file has different params from what we use to generate them')
wav_frames = wav_file.readframes(wav_file.getnframes())
frames = list(struct.unpack_from("%dh" % nframes, wav_frames))
return frames
99 changes: 99 additions & 0 deletions bfsk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""
BFSK ASCII kodiranje i dekodiranje
python3 bfsk.py enc "Bonsoir Elliot :)" 0.1 output.wav
enkodira "Bonsoir Elliot :)" koristeci 0.1s za trajanje bita i upisuje rezultat u output.wav
python3 bfsk.py dec input.wav
dekodira sadrzaj fajla input.wav i ispisuje rezultat
"""
import audiolib as a
import sys
import itertools

# top level params, can be tuned
freq_lo = 300.0 # frequency that represents 0
freq_hi = 1000.0 # frequency that represents 1

# Binary String -> ASCII Char conversion
def binstr2asciichar(str):
return chr(int(str, 2))

# Binary String <- ASCII Char conversion
def asciichar2binstr(ch):
return bin(ord(ch))[2:].zfill(8)

# encodes the message using bit_len as the bit length in seconds and writes it to file
def encode(message, bit_len, wav_filename):
print('Message:\"' + message + '\"')
# converts the message to binary ascii
encoded_message = ''.join([asciichar2binstr(ch) for ch in message])
print('Binary ASCII:\"' + encoded_message + '\"')
# creates frames
frames_per_bit = int(a.framerate * bit_len)
frames = []
phase = 0
# the main ecnoding loop
for bit in encoded_message:
# encodes one bit
freq = freq_hi if bit == '1' else freq_lo
new_frames, phase = a.create_frames(freq, frames_per_bit, phase)
frames.extend(new_frames)
# writes to file
a.write_wav(frames, wav_filename)
print('Wrote to file:\"' + wav_filename + '\"')

# this is where the magic happens, takes an array of frames and returns a message string
def extract_bits(frames):
# gets frequencies from frames
freqs_frames = a.extract_freqs_noob(frames)

# converts frequencies to bits, uses the average of all frequencies as a threshold
sum_all_freqs = sum([freq * frames for (freq, frames) in freqs_frames])
total_num_frames = sum([frames for (freq, frames) in freqs_frames])
freq_thresh = sum_all_freqs / total_num_frames
bits_frames = [( (freq > freq_thresh), frames) for freq, frames in freqs_frames]

# tuples get expanded and now we get an array of bits, one bit for each frame
bits_expanded = a.flatten([[freq] * frames for (freq, frames) in bits_frames])
# groups consecutive blocks of same values and filters out "really short blocks" that might
# be a result of noise (magic constant 10)
bits_grouped = [list(g) for k, g in itertools.groupby(bits_expanded)]
bits_filtered = [ (group[0], len(group)) for group in bits_grouped if len(group) > 10]

# doesn't know frames_per_bit, assumes that it's equal to the length
# of the shortest block of same bits (relies on the assumption that input has 101/010 somewhere)
maybe_frames_per_bit = min( [num_frames for bit, num_frames in bits_filtered] )
# adds bits to the return string
bits = a.flatten([[str(int(bit))] * round(num_frames / maybe_frames_per_bit) for bit, num_frames in bits_filtered])
return ''.join(bits)

# decodes wav_filename.wav
# > ugly hacks that work should get full marks
def decode(wav_filename):
# opens input file and gets frames
frames = a.read_wav(wav_filename)
print('Read from file:\"' + wav_filename + '\"')
encoded_message = extract_bits(frames)
print('Binary ASCII:\"' + encoded_message + '\"')
bin_strings = [''.join(bin_list) for bin_list in a.chunks(encoded_message, 8)]
message = ''.join([binstr2asciichar(bin_str) for bin_str in bin_strings])
print('Message:\"' + message + '\"') # solution

# main function, takes care of parsing the command line arguments
def main():
if len(sys.argv) < 2:
raise Exception('no arguments')
if sys.argv[1] == 'enc':
if len(sys.argv) != 5:
raise Exception('enc expects exactly three arguments')
encode(sys.argv[2], float(sys.argv[3]), sys.argv[4])
elif sys.argv[1] == 'dec':
if len(sys.argv) != 3:
raise Exception('dec expects exactly one argument')
decode(sys.argv[2])
else:
raise Exception('enc/dec are only allowed commands')

# entry point
if __name__ == "__main__":
main()
2 changes: 2 additions & 0 deletions gen_2.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
python3 bfsk.py enc "#Rand0m joined as R #Micro joined as M #Rand0m joined as R M: Oj R: Oj M: Imas li i dalje onu referencu za SSTV MrRobot B&W n8? R: Imam, cek sekund R: http://pastebin.com/raw/nsaByFUF R: Sta ce ti? M: Hocu da enkriptujem ono u treci fajl R: Aaa kul moze EOT" 0.1 2.wav
python3 bfsk.py dec 2.wav
2 changes: 2 additions & 0 deletions gen_3.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
python3 sstv.py enc qr.jpg 3.wav
python3 sstv.py dec 3.wav qr_sol.jpg
2 changes: 2 additions & 0 deletions gen_tekst.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
python3 morse.py enc "STX U VREME KADA NE RASPRAVLJAJU O VALIDNOSTI POLITICKIH OPCIJA KOJE ZASTUPAJU, RANDOM I MAJKRO OBOZAVAJU DA SVOJE RAZGOVORE PRENOSE NA NAJNEKONVENCIONALNIJE NACINE. PRED VAMA SU 3 ZVUCNA FAJLA. VAS ZADATAK JE DA RAZBIJETE OVE NSA PROOF METODE MODULACIJE I ENKODIRANJA I OTKRIJETE STA SE KRIJE SA DRUGE STRANE. AKO CITATE OVAJ TEKST, A NISTE VARALI, ONDA VAM CESTITAMO, JER STE PRVI DEO ZADATKA USPESNO URADILI. OD VAS SE DALJE OCEKUJE DA DEKODIRATE PREOSTALA DVA FAJLA I DOSTAVITE REZULTAT ZAJEDNO SA IZVORNIM KODOVIMA PROGRAMA KOJE STE NAPISALI KAKO BI DO REZULTATA DOSLI, UKLJUCUJUCI I KOD KOJIM STE DOSLI DO OVOG TEKSTA. RESENJA ZASNOVANA NA DOBROM SLUHU, TUDJIM KODOVIMA I VLASKOJ MAGIJI CE BITI ZNATNO MANJE BODOVANA. JOS JEDNA SITNICA: DRUGI FAJL JE BFSK MODULISAN ASCII TEKST SA 8 BITA PO KARAKTERU, A IZA TRECEG FAJLA SE KRIJE NAGRADA ZA NAJBRZEG. SRECNO ETX EOF" 0.06 tekst.wav
python3 morse.py dec tekst.wav
7 changes: 7 additions & 0 deletions img_convert.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from PIL import Image
import sys

img = Image.open("qr.jpg")
print(img.format, img.mode, img.size)
img = img.convert("1")
img.save("qr.jpg")
149 changes: 149 additions & 0 deletions morse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"""
Morse kodiranje i dekodiranje
python3 morse.py enc "HELLO FRIEND" 0.06 output.wav
enkodira "HELLO FRIEND" koristeci 0.06s za trajanje tacke i upisuje rezultat u output.wav
python3 morse.py dec input.wav
dekodira sadrzaj fajla input.wav i ispisuje rezultat
"""
import audiolib as a
import sys

# top level params, can be tuned
freq = 300.0 # frequency

# morse code dictionary, space maps to space for convenience
morse = {'A': '.-', 'B': '-...', 'C': '-.-.',
'D': '-..', 'E': '.', 'F': '..-.',
'G': '--.', 'H': '....', 'I': '..',
'J': '.---', 'K': '-.-', 'L': '.-..',
'M': '--', 'N': '-.', 'O': '---',
'P': '.--.', 'Q': '--.-', 'R': '.-.',
'S': '...', 'T': '-', 'U': '..-',
'V': '...-', 'W': '.--', 'X': '-..-',
'Y': '-.--', 'Z': '--..',
'0': '-----', '1': '.----', '2': '..---',
'3': '...--', '4': '....-', '5': '.....',
'6': '-....', '7': '--...', '8': '---..',
'9': '----.',
' ': ' ', '.': '.-.-.-', ',': '--..--', ':': '---...'
}

# encodes the message using tick_len as the unit length and creates wav_filename.wav
def encode(message, tick_len, wav_filename):
print('Message:\"' + message + '\"')
# converts the message to text morse
# after encoding we get 1 space between letters and 3 between words
encoded_message = ''.join([symbol for letter in ' '.join(message) for symbol in morse[letter]])

print('Text morse:\"' + encoded_message + '\"')

# builds the array of frames
frames_per_tick = int(a.framerate * tick_len)
frames = []
phase = 0
# the main ecnoding loop
for symbol in encoded_message:
if symbol == ' ':
new_frames, phase = a.create_frames(0, 2*frames_per_tick, phase)
frames.extend(new_frames) # letter break = 3, word break = 7
if symbol == '.':
new_frames, phase = a.create_frames(0, 1*frames_per_tick, phase)
frames.extend(new_frames) # symbol break = 1
new_frames, phase = a.create_frames(freq, 1*frames_per_tick, phase)
frames.extend(new_frames) # dit = 1
elif symbol == '-':
new_frames, phase = a.create_frames(0, 1*frames_per_tick, phase)
frames.extend(new_frames) # symbol break = 1
new_frames, phase = a.create_frames(freq, 3*frames_per_tick, phase)
frames.extend(new_frames) # dah = 3

# writes to file
a.write_wav(frames, wav_filename)
print('Wrote to file:\"' + wav_filename + '\"')

# adds one block to the array of blocks
def add_block(blocks, curr_block):
blocks.append(curr_block)
# if there is a really short useless block, removes it and merges two surrounding ones
if len(blocks) >= 3 and abs(blocks[-2]) == 1:
blocks.pop()
blocks.pop()
blocks[-1] += curr_block+1

# goes through frames and extracts blocks of sines/silences
def extract_blocks(frames):
# tries to adapt to various tick_lenghts
# kicks really short blocks out (probably noise)
blocks = []
curr_block = 0 # silent blocks are represented by negative values
last_frame = 0
for frame in frames:
if frame == 0: # silent frame
# registers the end of the sine block
if curr_block > 0:
add_block(blocks, curr_block)
curr_block = 0;
curr_block -= 1
else: # sine frame
# registers the end of the silent block
if curr_block < 0:
add_block(blocks, curr_block)
curr_block = 0;
curr_block += 1
# last block
add_block(blocks, curr_block)
# if last blocks is +-1, delete it
if len(blocks) and abs(blocks[-1]) == 1:
blocks.pop()
return blocks

# decodes wav_filename.wav
# > ugly hacks that work should get full marks
def decode(wav_filename):
# opens input file and gets frames
frames = a.read_wav(wav_filename)
print('Read from file:\"' + wav_filename + '\"')
# goes through frames and extracts blocks of sines/silences
blocks = extract_blocks(frames)
# determines the lengths of 5 possible atoms: word_break, letter_break, symbol_break, dit, dah
block_lengths = sorted(set(blocks))
if len(block_lengths) != 5:
print(block_lengths)
raise Exception('more or less than 5 different block lengths, oops')
# yes, I know what you're thinking, another crazy assumption
# we can be clever with less than 5 different block lengths but let's
# just leave it for now and assume that all 5 block types will be present
# we have 5 sorted block lengths
len_to_atom = {
block_lengths[0]: '# #', # word_break
block_lengths[1]: '#', # letter_break
block_lengths[2]: '', # symbol_break, not needed anymore
block_lengths[3]: '.', # dit
block_lengths[4]: '-' # dah
}
encoded_message = ''.join([len_to_atom[block] for block in blocks])
print('Text morse:\"' + encoded_message.replace('#', ' ') + '\"') # debug print
# makes an inverse mapping and decodes text morse to plaintext
inv_morse = {v: k for k, v in morse.items()}
message = ''.join([inv_morse[letter] for letter in encoded_message.split('#')])
print('Message:\"' + message + '\"') # solution

# main function, takes care of parsing the command line arguments
def main():
if len(sys.argv) < 2:
raise Exception('no arguments')
if sys.argv[1] == 'enc':
if len(sys.argv) != 5:
raise Exception('enc expects exactly three arguments')
encode(sys.argv[2], float(sys.argv[3]), sys.argv[4])
elif sys.argv[1] == 'dec':
if len(sys.argv) != 3:
raise Exception('dec expects exactly one argument')
decode(sys.argv[2])
else:
raise Exception('enc/dec are only allowed commands')

# entry point
if __name__ == "__main__":
main()
Binary file added qr.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added qr_sol.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit d8a21f2

Please sign in to comment.