├── requirements.txt ├── .DS_Store ├── rofl.wav ├── same.wav ├── test.wav ├── README.md ├── import numpy as np.py ├── same.py └── same-with-webserver.py /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | scipy -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicksmadscience/eas-same-encoder/HEAD/.DS_Store -------------------------------------------------------------------------------- /rofl.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicksmadscience/eas-same-encoder/HEAD/rofl.wav -------------------------------------------------------------------------------- /same.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicksmadscience/eas-same-encoder/HEAD/same.wav -------------------------------------------------------------------------------- /test.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicksmadscience/eas-same-encoder/HEAD/test.wav -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Emergency Alert System (EAS) Specific Area Message Encoding (SAME) Encoder 2 | 3 | [Here's a quick background on how EAS SAME headers work.](https://www.youtube.com/watch?v=Z5o1sfXXf9E) 4 | 5 | Since the mid-90's, a a device has sat in the headend room of every TV / FM / AM station that listens for EAS SAME signals on neighboring stations and, if the specified information matches the predefined filters for location and type of emergency, rebroadcasts the emergency message on its local station. 6 | 7 | Because I couldn't find an app that didn't require ten thousand dependencies, I wrote a Python script that generates these tones! Tested and is correctly interpreted by my SAGE 8 | EAS ENDEC. 9 | 10 | [YouTube demo!](https://www.youtube.com/watch?v=OVxHkMDX2F8) 11 | 12 | # Important Note 13 | 14 | Please please please please PLEASE use this responsibly. You will get in a lot of trouble if you send these over the airwaves. But if you just bought an old ENDEC on eBay and want to put it through its paces, this is the script for you! 15 | 16 | Also if you create a fork of this repo, please credit the [original project](https://github.com/nicksmadscience/eas-same-encoder/) :) 17 | -------------------------------------------------------------------------------- /import numpy as np.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.io import wavfile 3 | import random 4 | import sys 5 | 6 | fs = 43750 7 | 8 | # t = 1.0 / (520 + (5/6)) 9 | 10 | 11 | # f = 2083.33333 12 | 13 | samples = np.zeros(0) 14 | 15 | 16 | def markBit(): 17 | f = 2083.33333 18 | t = 1.0 / (520 + (5/6)) 19 | 20 | samples = np.arange(t * fs) / fs 21 | 22 | roffle = np.sin(2 * np.pi * f * samples) 23 | return roffle * 0.8 24 | 25 | def spaceBit(): 26 | f = 1562.5 27 | t = 1.0 / (520 + (5/6)) 28 | 29 | samples = np.arange(t * fs) / fs 30 | 31 | return np.sin(2 * np.pi * f * samples) 32 | 33 | 34 | 35 | signal = np.zeros(20000) 36 | 37 | 38 | def byte(the_byte): 39 | sys.stdout.write(the_byte) 40 | sys.stdout.write(" ") 41 | byte_data = np.zeros(0) 42 | for i in range(0, 8): 43 | if ord(the_byte) >> i & 1: 44 | sys.stdout.write("1") 45 | byte_data = np.append(byte_data, markBit()) 46 | else: 47 | sys.stdout.write("0") 48 | byte_data = np.append(byte_data, spaceBit()) 49 | 50 | sys.stdout.write("\n") 51 | sys.stdout.flush() 52 | 53 | return byte_data 54 | 55 | 56 | def extramarks(numberOfMarks): 57 | """SAGE encoders seem to add a few mark bits at the beginning and end""" 58 | byte_data = np.zeros(0) 59 | 60 | for i in range(0, numberOfMarks): 61 | byte_data = np.append(byte_data, markBit()) 62 | 63 | return byte_data 64 | 65 | def preamble(): 66 | byte_data = np.zeros(0) 67 | 68 | for i in range(0, 16): 69 | byte_data = np.append(byte_data, markBit()) 70 | byte_data = np.append(byte_data, markBit()) 71 | byte_data = np.append(byte_data, spaceBit()) 72 | byte_data = np.append(byte_data, markBit()) 73 | byte_data = np.append(byte_data, spaceBit()) 74 | byte_data = np.append(byte_data, markBit()) 75 | byte_data = np.append(byte_data, spaceBit()) 76 | byte_data = np.append(byte_data, markBit()) 77 | 78 | 79 | 80 | return byte_data 81 | 82 | 83 | 84 | 85 | # ZCZC-WXR-RWT-020103-020209-020091-020121-029047-029165-029095-029037+0030-1051700-KEAX/NWS 86 | 87 | # code = "ZCZC-EAS-RMT-011000+0100-2141800-SCIENCE-" 88 | code = "ZCZC-WXR-HUW-024031+0030-2142201-SCIENCE -" 89 | # code = "ZCZC-PEP-EAT-000000+0400-2142300-SCIENCE -" 90 | # code = "SUCK MY FUCKING BALLS YOU FUCKING COCKSUCKERS" 91 | 92 | # control string 93 | # code = "ZCZC-EAS-RMT-011000+0100-2142200-KMMS FM -" 94 | 95 | for i in range(0, 3): 96 | signal = np.append(signal, extramarks(10)) 97 | signal = np.append(signal, preamble()) 98 | 99 | 100 | for char in code: 101 | signal = np.append(signal, byte(char)) 102 | 103 | signal = np.append(signal, extramarks(6)) 104 | 105 | signal = np.append(signal, np.zeros(43750)) 106 | 107 | 108 | for i in range(0, 3): 109 | signal = np.append(signal, extramarks(10)) 110 | signal = np.append(signal, preamble()) 111 | 112 | for char in "NNNN": 113 | signal = np.append(signal, byte(char)) 114 | 115 | signal = np.append(signal, extramarks(6)) 116 | 117 | signal = np.append(signal, np.zeros(43750)) 118 | 119 | 120 | 121 | signal *= -32767 122 | 123 | signal = np.int16(signal) 124 | 125 | wavfile.write(str("same.wav"), fs, signal) 126 | 127 | 128 | -------------------------------------------------------------------------------- /same.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """\ 3 | Generates valid EAS SAME messages and tones. 4 | 5 | Usage: python3 same.py --code=(code) --filename=(filename) 6 | """ 7 | __author__ = "Nick Piegari" 8 | __license__ = "GPL" 9 | __version__ = "1.0" 10 | __email__ = "nickpiegari@gmail.com" 11 | 12 | 13 | import numpy as np 14 | import scipy.io.wavfile 15 | import datetime # EAS alerts are heavily dependent on timestamps so this makes it easy to send a thing now 16 | import argparse 17 | 18 | 19 | 20 | class SAME: 21 | """Generates valid EAS SAME messages and tones.""" 22 | 23 | def __init__(self): 24 | ######## CONFIG / constants ######## 25 | 26 | self.samplerate = 43750 # makes for easy lowest common denominatorization. not tested at any other rate 27 | 28 | 29 | def markBit(self): 30 | """Generates a mark bit, four cycles of 2083 1/3 Hz.""" 31 | f = 2083.33333 32 | t = 1.0 / (520 + (5/6)) 33 | 34 | samples = np.arange(t * self.samplerate) / self.samplerate 35 | 36 | return np.sin(2 * np.pi * f * samples) 37 | 38 | 39 | def spaceBit(self): 40 | """Generates a space bit, four cycles of 1562.5Hz.""" 41 | f = 1562.5 42 | t = 1.0 / (520 + (5/6)) 43 | 44 | samples = np.arange(t * self.samplerate) / self.samplerate 45 | 46 | return np.sin(2 * np.pi * f * samples) 47 | 48 | 49 | 50 | def byte(self, the_byte): 51 | """Turns the provided byte into FSK data.""" 52 | byte_data = np.zeros(0) 53 | for i in range(0, 8): 54 | if ord(the_byte) >> i & 1: 55 | byte_data = np.append(byte_data, self.markBit()) 56 | else: 57 | byte_data = np.append(byte_data, self.spaceBit()) 58 | 59 | return byte_data 60 | 61 | 62 | def preamble(self): 63 | """Builds the sixteen-byte 10101011 preamble.""" 64 | byte_data = np.zeros(0) 65 | 66 | for i in range(0, 16): 67 | byte_data = np.append(byte_data, self.markBit()) 68 | byte_data = np.append(byte_data, self.markBit()) 69 | byte_data = np.append(byte_data, self.spaceBit()) 70 | byte_data = np.append(byte_data, self.markBit()) 71 | byte_data = np.append(byte_data, self.spaceBit()) 72 | byte_data = np.append(byte_data, self.markBit()) 73 | byte_data = np.append(byte_data, self.spaceBit()) 74 | byte_data = np.append(byte_data, self.markBit()) 75 | 76 | return byte_data 77 | 78 | 79 | def attentiontone(self, length=5): 80 | """Generates a standard two-tone attention signal.""" 81 | tone = np.sin(2 * np.pi * 853 * np.arange(0, length, 1/self.samplerate)) 82 | tone2 = np.sin(2 * np.pi * 960 * np.arange(0, length, 1/self.samplerate)) 83 | 84 | return (tone + tone2) / 2 85 | 86 | 87 | 88 | def noaatone(self, length=5): 89 | """Generates a weather-radio-style 1050Hz tone...........""" 90 | return np.sin(2 * np.pi * 1050 * np.arange(0, length, 1/self.samplerate)) 91 | 92 | 93 | def pause(self, length=1): 94 | """Generates a period of silence for the specified duration.""" 95 | return np.zeros(int(self.samplerate * length)) 96 | 97 | 98 | def buildMessage(self, code, filename="same.wav", tone="eas", db=-3.0): 99 | """Generates and writes to file a complete EAS SAME message..""" 100 | 101 | # STEP ONE: Insert a bit of silence 102 | signal = self.pause(0.5) 103 | 104 | 105 | # message (3x) 106 | for i in range(0, 3): 107 | signal = np.append(signal, self.preamble()) 108 | 109 | # turn each character into a sequence of sine waves 110 | for char in code: 111 | signal = np.append(signal, self.byte(char)) 112 | 113 | signal = np.append(signal, self.pause(1)) # wait the requisite one second 114 | 115 | 116 | # tone. will be skipped if it's not 'eas' or 'tone' 117 | if tone == "eas": 118 | signal = np.append(signal, self.attentiontone()) 119 | signal = np.append(signal, self.pause(1)) 120 | elif tone == "noaa": 121 | signal = np.append(signal, self.noaatone()) 122 | signal = np.append(signal, self.pause(1)) 123 | 124 | 125 | # EOM (3x) 126 | for i in range(0, 3): 127 | signal = np.append(signal, self.preamble()) 128 | 129 | for char in "NNNN": # NNNN = End Of Message 130 | signal = np.append(signal, self.byte(char)) 131 | 132 | signal = np.append(signal, self.pause(1)) # wait the requisite one second 133 | 134 | 135 | # wave-ify and adjust trim (decibel to normal conversion) 136 | signal *= 32767 * (10.0 ** (db / 20.0)) 137 | 138 | signal = np.int16(signal) 139 | 140 | scipy.io.wavfile.write(filename, self.samplerate, signal) 141 | 142 | 143 | 144 | 145 | 146 | if __name__ == "__main__": 147 | # EAS alerts are heavily dependent on timestamps so this makes it easy/fun to send a thing now 148 | sameCompatibleTimestamp = datetime.datetime.now().strftime("%j%H%M") 149 | 150 | # parse command-line arguments 151 | parser = argparse.ArgumentParser() 152 | parser.add_argument("--code", "-c", nargs='?', 153 | default="ZCZC-EAS-RWT-000000+0400-" + sameCompatibleTimestamp + "-SCIENCE -", 154 | help="The message to be SAME-ified. Can be anything, but only valid messages will be decoded by an EAS device.") 155 | parser.add_argument("--filename", "-f", default="same.wav", help="Writes to this file.") 156 | parser.add_argument("--tone", "-t", default="eas", help="Whether to use EAS tone, NOAA tone, or no tone.") 157 | parser.add_argument("--db", "-d", default=-3.0, help="Trim adjustment in decibels. Anything over 0dB will distort and will probably not decode correctly.") 158 | args = parser.parse_args() 159 | 160 | print (args.code) 161 | 162 | same = SAME() 163 | 164 | same.buildMessage(args.code, filename=args.filename, tone=args.tone, db=float(args.db)) 165 | 166 | -------------------------------------------------------------------------------- /same-with-webserver.py: -------------------------------------------------------------------------------- 1 | # EAS SAME audio file generator! 2 | # Written in a single sitting on a lazy Saturday. 3 | # I thought this would take WAY more time than it did 4 | # most of the development time was getting it to work with my SAGE EAS ENDEC 5 | # (which is understandably very prone to sanity-checking) 6 | 7 | # TODO: per development best practices, this should be class-ified 8 | 9 | 10 | 11 | import numpy as np 12 | from scipy.io import wavfile 13 | # import random # used for testing gibberish data during early development 14 | import sys # for stdout 15 | import subprocess # to play the resulting wave file 16 | import datetime # EAS alerts are heavily dependent on timestamps so this makes it easy to send a thing now 17 | 18 | 19 | class easSAMEHeader: 20 | def __init__(self, same_header_string): 21 | self.same_header_string = same_header_string 22 | 23 | self.fs = 43750 24 | 25 | # get a blank 'samples' ready; this is where we'll build up the alert 26 | samples = np.zeros(0) 27 | 28 | 29 | def markBit(): 30 | f = 2083.33333 31 | t = 1.0 / (520 + (5/6)) 32 | 33 | samples = np.arange(t * fs) / fs 34 | 35 | roffle = np.sin(2 * np.pi * f * samples) 36 | return roffle * 0.8 37 | 38 | def spaceBit(): 39 | f = 1562.5 40 | t = 1.0 / (520 + (5/6)) 41 | 42 | samples = np.arange(t * fs) / fs 43 | 44 | return np.sin(2 * np.pi * f * samples) 45 | 46 | 47 | 48 | signal = np.zeros(20000) 49 | 50 | 51 | def byte(the_byte): 52 | sys.stdout.write(the_byte) 53 | sys.stdout.write(" ") 54 | byte_data = np.zeros(0) 55 | for i in range(0, 8): 56 | if ord(the_byte) >> i & 1: 57 | sys.stdout.write("1") 58 | byte_data = np.append(byte_data, markBit()) 59 | else: 60 | sys.stdout.write("0") 61 | byte_data = np.append(byte_data, spaceBit()) 62 | 63 | sys.stdout.write("\n") 64 | sys.stdout.flush() 65 | 66 | return byte_data 67 | 68 | 69 | def extramarks(numberOfMarks): 70 | """SAGE encoders seem to add a few mark bits at the beginning and end""" 71 | byte_data = np.zeros(0) 72 | 73 | for i in range(0, numberOfMarks): 74 | byte_data = np.append(byte_data, markBit()) 75 | 76 | return byte_data 77 | 78 | def preamble(): 79 | byte_data = np.zeros(0) 80 | 81 | for i in range(0, 16): 82 | byte_data = np.append(byte_data, markBit()) 83 | byte_data = np.append(byte_data, markBit()) 84 | byte_data = np.append(byte_data, spaceBit()) 85 | byte_data = np.append(byte_data, markBit()) 86 | byte_data = np.append(byte_data, spaceBit()) 87 | byte_data = np.append(byte_data, markBit()) 88 | byte_data = np.append(byte_data, spaceBit()) 89 | byte_data = np.append(byte_data, markBit()) 90 | 91 | # TODO: i could probably replace this with the bits() function 92 | 93 | 94 | 95 | 96 | return byte_data 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | # EAS alerts are heavily dependent on timestamps so this makes it easy/fun to send a thing now 105 | sameCompatibleTimestamp = datetime.datetime.now().strftime("%j%H%M") 106 | 107 | # known good 108 | code = "ZCZC-PEP-EAN-000000+0400-" + sameCompatibleTimestamp + "-SCIENCE -" # nuclear armageddon (or some other form of "we are all likely to die") 109 | code = "ZCZC-PEP-EAT-000000+0400-" + sameCompatibleTimestamp + "-SCIENCE -" # nuclear armageddon (or some other form of "we are all likely to die") 110 | # code = "ZCZC-PEP-EAT-000000+0400-2142350-SCIENCE -" # lol jk no nuclear armageddon 111 | # code = "ZCZC-WXR-TOR-024031+0030-2150015-SCIENCE -" # tornado warning, silver spring, md 112 | # code = "ZCZC-WXR-SVR-024031+0030-2142200-SCIENCE -" # severe thunderstorm warning, silver spring, md 113 | # code = "ZCZC-WXR-EVI-024031+0030-2150010-SCIENCE -" # evacuation immediate!!, silver spring, md 114 | # code = "ZCZC-WXR-FFW-024031+0030-2150021-SCIENCE -" 115 | # code = "SUCK MY F**KING B***S YOU F**KING C*********RS" # does not seem to work :'( 116 | 117 | 118 | for i in range(0, 3): 119 | # signal = np.append(signal, extramarks(10)) 120 | signal = np.append(signal, preamble()) 121 | 122 | # turn each character into a sequence of sine waves 123 | for char in code: 124 | signal = np.append(signal, byte(char)) 125 | 126 | # signal = np.append(signal, extramarks(6)) # ENDEC might not be as picky about this as I once thought 127 | 128 | signal = np.append(signal, np.zeros(43750)) # wait the requisite one second 129 | 130 | 131 | # EOM (3x) 132 | for i in range(0, 3): 133 | # signal = np.append(signal, extramarks(10)) 134 | signal = np.append(signal, preamble()) 135 | 136 | for char in "NNNN": # NNNN = End Of Message 137 | signal = np.append(signal, byte(char)) 138 | 139 | # signal = np.append(signal, extramarks(6)) 140 | 141 | signal = np.append(signal, np.zeros(43750)) # wait the requisite one second 142 | 143 | 144 | 145 | 146 | 147 | 148 | signal *= -32767 149 | 150 | signal = np.int16(signal) 151 | 152 | wavfile.write(str("same.wav"), fs, signal) 153 | 154 | 155 | subprocess.call("afplay same.wav", shell=True) 156 | 157 | 158 | def generateSAMEAudioBasedOnCode(sameCode): 159 | 160 | 161 | 162 | 163 | 164 | def requestHandler_samecode(_get): 165 | """Generate and play back an EAS SAME header code based on _get""" 166 | global clients 167 | 168 | print urllib2.unquote(_get[2]) 169 | 170 | # for client in clients: 171 | # client.write_message(json.dumps({"messagetype": "marquee", "message": urllib2.unquote(_get[2])})) 172 | 173 | return "text/plain", str(_get[2]) 174 | 175 | 176 | 177 | httpRequests = {'': requestHandler_index, 178 | 'samecode': requestHandler_samecode, 179 | } 180 | 181 | 182 | 183 | #This class will handles any incoming request from 184 | #the browser 185 | class myHandler(BaseHTTPRequestHandler): 186 | 187 | #Handler for the GET requests 188 | def do_GET(self): 189 | elements = self.path.split('/') 190 | 191 | responseFound = False 192 | for httpRequest, httpHandler in httpRequests.iteritems(): 193 | # print elements[1] + " == " + httpRequest 194 | if elements[1] == httpRequest: # in other words, if the first part matches 195 | contentType, response = httpHandler(elements) 196 | responseFound = True 197 | 198 | self.send_response(200) 199 | self.send_header("Access-Control-Allow-Origin", "*") 200 | self.send_header('Content-type', contentType) 201 | self.end_headers() 202 | 203 | self.wfile.write(response) 204 | if not responseFound: 205 | contentType, response = requestHandler_index('/') 206 | 207 | self.send_response(200) 208 | self.send_header("Access-Control-Allow-Origin", "*") 209 | self.send_header('Content-type', contentType) 210 | self.end_headers() 211 | 212 | self.wfile.write(response) 213 | 214 | return 215 | 216 | 217 | def http(): 218 | server = HTTPServer(('', PORT_NUMBER), myHandler) 219 | print 'Started httpserver on port ' , PORT_NUMBER 220 | 221 | server.serve_forever() 222 | 223 | httpThread = Thread(target=http) 224 | httpThread.daemon = True 225 | httpThread.start() 226 | 227 | --------------------------------------------------------------------------------