#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()
