#This script is used to broadcast either the current system time and date or a
#user defined time and date in the form of the UK MSF radio signal.
#
#It uses the computer's soundcard to generate an AC signal with a harmonic that
#matches the frequency of the MSF signal.
#
#For best results it is recommended that a broadcast antenna is made. An example
#of this can be found at http://www.burningimage.net/msfsimulator
#
#This script has been tested as working in Python 3.6.8
# Script requisites
from datetime import datetime,timedelta
import pyaudio
import numpy
import math
def sine(frequency, length, rate):
length = int(length * rate)
factor = float(frequency) * (math.pi * 2) / rate
return numpy.sin(numpy.arange(length) * factor)
def play_tone(stream, frequency=440, length=10, rate=48000):
chunks = []
#overdrive it to a square wave
chunks.append(1e3*sine(frequency, length, rate))
chunk = numpy.concatenate(chunks) * 0.25
stream.write(chunk.astype(numpy.float32).tostring())
def minutemarker():
play_tone(stream, 0, 0.5)
play_tone(stream, 20000, 0.5)
def send01():
play_tone(stream, 0, 0.1)
play_tone(stream, 20000, 0.1)
play_tone(stream, 0, 0.1)
play_tone(stream, 20000, 0.7)
def send11():
play_tone(stream, 0, 0.3)
play_tone(stream, 20000, 0.7)
def send00():
play_tone(stream, 0, 0.1)
play_tone(stream, 20000, 0.9)
def send10():
play_tone(stream, 0, 0.2)
play_tone(stream, 20000, 0.8)
bcdlist= [80, 40, 20, 10, 8, 4, 2, 1]
p = pyaudio.PyAudio()
stream = p.open(format=pyaudio.paFloat32, channels=1, rate=48000, output=1)
# Splash screen and mode select
print("MSF Signal Broadcaster"
"\nBased on code by Andy (burningimage.net/msfsimulator)\n"
"\nFor best results:"
"\n - Minimum recommended broadcast time is 10 minutes"
"\n - Turn system volume to 100%"
"\n - Connect an RF antenna to audio out port (DIY instruction at burningimage.net)\n"
"\n[1] Broadcast signal using current system date and time"
"\n - For accuracy, ensure system time is correct before selecting\n"
"\n[2] Broadcast signal using custom date and time"
"\n - User will be prompted to enter values\n")
choice = False # To skip selection, change to 1 (system time) or 2 (custom time)
while 1 > choice or 2 < choice:
try:
choice = int(input("Please choose a broadcast option (1-2): "))
except ValueError:
print("Invalid entry. Please try again.\n")
# Choose time in mimutes to broadcast for
broadcast_time = False # To skip selction, change to value 1-60
while 1 > broadcast_time or 60 < broadcast_time:
try:
broadcast_time = int(input("Please choose how long to broadcast for (1-60 minutes): "))
except ValueError:
print("Invalid entry. Please try again.\n")
# System time chosen - wait for seconds to reach 0
if choice == 1:
print("\nPlease wait. Signal will commence when system time reaches the next minute.")
while datetime.now().second != 0:
pass
# Set the time and date to match the system time and date plus 1 minute (MSF broadcasts the incoming rather than the current minute)
now = datetime.now() + timedelta(minutes=1)
# Custom time chosen - request user input
if choice == 2:
year, month, dayofmonth, dayofweek, hour, minute = (100,)*6
while 2000 > year or 2099 < year:
try:
year = int(input("Please input year (2000-2099): "))
except ValueError:
print("Invalid entry. Please try again.\n")
while 1 > month or 12 < month:
try:
month = int(input("Please input month (1-12): "))
except ValueError:
print("Invalid entry. Please try again.\n")
if month == (4 or 6 or 9 or 11): #Limiting input to appropriate number of days depending on selected month
month_end = 30
elif month == 2:
if year % 4 != 0 or (year % 100 == 0 and year % 400 != 0): # Leap year rules
month_end = 28
else:
month_end = 29
else:
month_end = 31
while 1 > dayofmonth or month_end < dayofmonth:
try:
dayofmonth = int(input("Please input day of month (1-"+str(month_end)+"): "))
except ValueError:
print("Invalid entry. Please try again.\n")
while 0 > hour or 23 < hour:
try:
hour = int(input("Please input hour (0-23): "))
except ValueError:
print("Invalid entry. Please try again.\n")
while 0 > minute or 59 < minute:
try:
minute = int(input("Please input minute (0-59): "))
except ValueError:
print("Invalid entry. Please try again.\n")
now = datetime(year,month,dayofmonth,hour,minute) + timedelta(minutes=1) # Applies MSF minute correction as above
# Set to 1 to transmit proper parity bits
enableparity = 1
#-------------------------------------------------------------------
print("\nFiring up the signal!")
while broadcast_time > 0:
# Ignore the '0' element in the list as it confuses matters
# with the timecode
timecodeA = [0] * 60
timecodeB = [0] * 60
# Convert the year to BCD and store in the correct place in the timecode
bcdindex=0
temp = now.year%100
sum=0
for i in range(17,25):
if temp >= bcdlist[bcdindex]:
timecodeA[i] = 1
sum += 1;
temp -= bcdlist[bcdindex]
bcdindex += 1
# Work out the parity bit for 17-24
if (sum % 2) != 1:
timecodeB[54] = enableparity
# Now do the month
bcdindex=3 #starts at 10
temp = now.month
sum = 0
for i in range(25,30):
if temp >= bcdlist[bcdindex]:
timecodeA[i] = 1
temp -= bcdlist[bcdindex]
sum += 1
bcdindex += 1
# Do the day of month
bcdindex=2 #starts at 20
temp = now.day
for i in range(30,36):
if temp >= bcdlist[bcdindex]:
timecodeA[i] = 1
temp -= bcdlist[bcdindex]
sum += 1
bcdindex += 1
# Work out the parity bit for 25-35
if (sum % 2) != 1:
timecodeB[55] = enableparity
# Do the day of week
bcdindex=5 #starts at 4
temp = now.weekday() + 1
if temp > 6:
temp = 0 # Corrects for weekday returning Monday as 0
sum = 0
for i in range(36,39):
if temp >= bcdlist[bcdindex]:
timecodeA[i] = 1
temp -= bcdlist[bcdindex]
sum += 1
bcdindex += 1
# Work out the parity bit for 36-38
if (sum % 2) != 1:
timecodeB[56] = enableparity
# Do the hour
bcdindex=2 #starts at 20
temp = now.hour
sum = 0
for i in range(39,45):
if temp >= bcdlist[bcdindex]:
timecodeA[i] = 1
temp -= bcdlist[bcdindex]
sum += 1
bcdindex += 1
# Do the minute
bcdindex=1 #starts at 40
temp = now.minute
for i in range(45,52):
if temp >= bcdlist[bcdindex]:
timecodeA[i] = 1
temp -= bcdlist[bcdindex]
sum += 1
bcdindex += 1
# Work out the parity bit for 36-38
if (sum % 2) != 1:
timecodeB[57] = enableparity
# Bits 53A - 58A should always be 1
for i in range(53,59):
timecodeA[i] = 1
# Display the date and time without the MSF correction
dnow = now - timedelta(minutes=1)
print("\nBroadcasting: " + str(dnow.year).zfill(4) + "-" + str(dnow.month).zfill(2) + "-" + str(dnow.day).zfill(2) + " " + str(dnow.hour).zfill(2) + ":" + str(dnow.minute).zfill(2))
print(str(broadcast_time) + " minutes of broadcast remaining.")
# Now play the timecode out
minutemarker()
for i in range(1,60):
if (timecodeA[i] == 1) and (timecodeB[i] == 1):
send11()
elif (timecodeA[i] == 0) and (timecodeB[i] == 1):
send01()
elif (timecodeA[i] == 0) and (timecodeB[i] == 0):
send00()
elif (timecodeA[i] == 1) and (timecodeB[i] == 0):
send10()
# Increment date by 1 minute
now = now + timedelta(minutes=1)
# Decrement broadcast timer
broadcast_time -= 1
stream.close()
p.terminate()