Luis = B3 G#4 G#3 F#4
# Morse Code and Braille Dictionaries
MORSE_CODE_DICT = {
'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': '----.',
'.': '.-.-.-', ',': '--..--', '?': '..--..', "'": '.----.', '!': '-.-.--',
'/': '-..-.', '(': '-.--.', ')': '-.--.-', '&': '.-...', ':': '---...',
';': '-.-.-.', '=': '-...-', '+': '.-.-.', '-': '-....-',
'_': '..--.-', '"': '.-..-.', '@': '.--.-.', ' ': '/'
}
BRAILLE_DICT = {
'A': '100000', 'B': '101000', 'C': '110000', 'D': '110100', 'E': '100100',
'F': '111000', 'G': '111100', 'H': '101100', 'I': '011000', 'J': '011100',
'K': '100010', 'L': '101010', 'M': '110010', 'N': '110110', 'O': '100110',
'P': '111010', 'Q': '111110', 'R': '101110', 'S': '011010', 'T': '011110',
'U': '100011', 'V': '101011', 'W': '011101', 'X': '110011', 'Y': '110111',
'Z': '100111',
'0': '011111', '1': '100000', '2': '101000', '3': '110000', '4': '110100',
'5': '100100', '6': '111000', '7': '111100', '8': '101100', '9': '011000',
' ': '000000', '.': '100001', ',': '110000', '?': '001001', '!': '011010',
"'": '001000', '-': '100001', '/': '001101', '(': '010010', ')': '010011',
'&': '101000', ':': '010110', ';': '010011', '=': '100011', '+': '101001',
'_': '100101', '"': '001010', '@': '101100'
}
VALID_NOTES = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", "Ab", "Bb"}
def text_to_morse(text):
"""Convert text to Morse code."""
return [MORSE_CODE_DICT.get(char.upper(), '') for char in text]
def text_to_braille(text):
"""Convert text to Braille."""
return [BRAILLE_DICT.get(char.upper(), '000000') for char in text]
def print_morse_formatted(input_text, morse_code):
"""Print formatted Morse code representation."""
print("Morse Code Representation:")
for char, code in zip(input_text.upper(), morse_code):
print(f"\t\t{char} : {code}")
def print_braille_formatted(input_text, braille_code, translated_notes):
"""Print formatted Braille representation with translated notes."""
print("Braille Representation:")
for char, cell, notes in zip(input_text.upper(), braille_code, translated_notes):
print(f"\t\t{char} : {cell}\t{notes}")
def display_help():
"""Display help information."""
print("""
Welcome to the Text Converter!
Options:
1. Convert to Morse Code
- Enter text to convert to Morse code.
- The Morse code will be printed.
2. Convert to Braille
- Enter text to convert to Braille representation.
- The Braille code will be printed.
9. Help
- Displays this help information.
Notes:
- Morse Code: Uses dots (.) and dashes (-) to represent characters.
- Braille: Uses patterns of raised dots to represent characters.
- When entering a scale, ensure notes are separated by spaces (e.g., C D E F# G# Ab).
""")
def validate_notes(scale):
"""Validate the notes in the scale."""
for note in scale:
if note not in VALID_NOTES:
raise ValueError(f"The scale contains invalid notes: {note}. Please ensure all notes are correctly formatted and are part of the following set: {', '.join(VALID_NOTES)}.")
def check_scale_length(braille_code_list, scale):
"""Check if the scale has enough notes to cover the Braille code positions."""
num_positions = len(braille_code_list[0]) # Assume all Braille code entries have the same length
num_notes = len(scale)
if num_notes < num_positions:
raise ValueError("The scale provided has fewer notes than the number of positions in the Braille code. Please provide a scale with at least as many notes as there are positions in the Braille code.")
def handle_scale(scale, braille_code_list):
"""
Process the scale input and replace Braille code with notes from the scale.
"""
scale = scale.split() # Convert input string to list
try:
validate_notes(scale) # Validate notes in the scale
check_scale_length(braille_code_list, scale) # Check scale length
except ValueError as e:
print(f"Error: {e}")
print("Ensure the scale notes are separated by spaces.")
return None
# Map Braille code to scale
translated_notes = []
for braille in braille_code_list:
notes = [scale[i] for i, char in enumerate(braille) if char == '1']
translated_notes.append(' '.join(notes))
return translated_notes
def main():
"""Main function."""
print("Welcome to the Text Converter!")
print("Instructions:")
print("1 - Convert to Morse Code")
print("2 - Convert to Braille")
print("9 - Help")
choice = input("Enter your choice (1, 2, or 9): ").strip()
if choice == '9':
display_help()
return
if choice not in ['1', '2']:
print("Invalid choice. Please select 1, 2, or 9.")
return
input_text = input("Enter text to convert: ")
if choice == '1':
morse_code_list = text_to_morse(input_text)
morse_code = ' '.join(morse_code_list)
print(f"\nInput Text: {input_text}")
print(f"Morse Code: {morse_code}") # Line with the complete Morse code
print_morse_formatted(input_text, morse_code_list)
elif choice == '2':
braille_code_list = text_to_braille(input_text)
braille_code = ' '.join(braille_code_list)
print(f"\nInput Text: {input_text}")
print(f"Braille Code: {braille_code}")
# New User Input for Scale/Encoder
scale = input("Enter notes for Scale/Encoder (e.g., C D E F# G# Ab): ").strip()
translated_notes = handle_scale(scale, braille_code_list)
if translated_notes is not None:
print_braille_formatted(input_text, braille_code_list, translated_notes)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Braille <-> Music Cipher
========================
Idea:
1. Text -> Braille : each letter maps to a standard Grade-1 English
braille cell, which is a 2x3 grid of up to 6 dots:
1 4
2 5
3 6
2. Braille -> Music : each of the 6 dot positions is assigned a fixed
pitch. A letter's raised dots become the notes
that sound together (a chord). A letter with
dots {1, 2, 5} becomes a 3-note chord; a letter
with just {1} becomes a single note.
3. Music -> Braille -> Text (decoding) runs the whole thing in reverse,
so this is a fully reversible cipher: text -> chords -> text.
Customize the cipher by editing NOTE_MAP below -- that's the only truly
"creative" choice in the whole scheme (which pitch represents which dot).
Everything else (the braille alphabet) is a fixed, standard reference
table, so two people using this script with the SAME NOTE_MAP can encode
and decode each other's messages -- that shared NOTE_MAP is effectively
your shared "key".
Usage:
python braille_music_cipher.py configure
-> interactively pick which note goes with each of the 6 dot
positions; saves your choices to note_map.json so encode/decode
automatically use them from then on.
python braille_music_cipher.py encode "hello world"
python braille_music_cipher.py decode "C5 E4 | C5 A4 D4 | ..."
python braille_music_cipher.py encode "hello world" --midi out.mid
"""
import argparse
import json
import os
import re
import sys
# Where a custom note mapping gets saved/loaded from (lives next to this script,
# or wherever you run it from -- it's just a small JSON file).
CONFIG_FILE = "note_map.json"
# ---------------------------------------------------------------------------
# 1. Standard Grade-1 English braille alphabet (a-z), as sets of raised dots.
# Dot numbering:
# 1 4
# 2 5
# 3 6
# ---------------------------------------------------------------------------
BRAILLE_ALPHABET = {
"a": (1,),
"b": (1, 2),
"c": (1, 4),
"d": (1, 4, 5),
"e": (1, 5),
"f": (1, 2, 4),
"g": (1, 2, 4, 5),
"h": (1, 2, 5),
"i": (2, 4),
"j": (2, 4, 5),
"k": (1, 3),
"l": (1, 2, 3),
"m": (1, 3, 4),
"n": (1, 3, 4, 5),
"o": (1, 3, 5),
"p": (1, 2, 3, 4),
"q": (1, 2, 3, 4, 5),
"r": (1, 2, 3, 5),
"s": (2, 3, 4),
"t": (2, 3, 4, 5),
"u": (1, 3, 6),
"v": (1, 2, 3, 6),
"w": (2, 4, 5, 6),
"x": (1, 3, 4, 6),
"y": (1, 3, 4, 5, 6),
"z": (1, 3, 5, 6),
" ": (), # space = silence / rest, no dots raised
}
# Reverse lookup: dot-pattern (as a frozenset) -> letter
DOTS_TO_LETTER = {frozenset(v): k for k, v in BRAILLE_ALPHABET.items()}
# ---------------------------------------------------------------------------
# 2. Dot position -> pitch. EDIT THIS to define your own cipher "key".
# Note names use scientific pitch notation (C4 = middle C).
# Any note names are fine as long as encode and decode use the SAME map.
# ---------------------------------------------------------------------------
NOTE_MAP = {
1: "C5",
2: "A4",
3: "F4",
4: "E5",
5: "C4",
6: "G4",
}
# Reverse lookup: pitch -> dot position (built automatically from NOTE_MAP)
NOTE_TO_DOT = {v: k for k, v in NOTE_MAP.items()}
# MIDI note numbers for standard pitch names (needed only for --midi export)
_NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
# Valid note name pattern: a letter A-G, optional # (sharp), then an octave 0-9
# e.g. "C4", "F#5", "A0" ... but not "H4" or "C" (missing octave).
_NOTE_PATTERN = re.compile(r"^[A-Ga-g]#?\d$")
def is_valid_note(note: str) -> bool:
return bool(_NOTE_PATTERN.match(note.strip()))
def note_name_to_midi(note: str) -> int:
"""Convert e.g. 'C5' or 'F#4' to a MIDI note number."""
note = note.strip()
if len(note) == 3:
pitch, octave = note[:2], note[2]
else:
pitch, octave = note[0], note[1]
pitch = pitch[0].upper() + pitch[1:] # normalize e.g. "c#" -> "C#"
return _NOTE_NAMES.index(pitch) + (int(octave) + 1) * 12
def apply_note_map(new_map: dict):
"""Replace the active NOTE_MAP (and its reverse lookup) with new_map."""
global NOTE_MAP, NOTE_TO_DOT
NOTE_MAP = dict(new_map)
NOTE_TO_DOT = {v: k for k, v in NOTE_MAP.items()}
def load_note_map() -> dict:
"""Load a saved custom note map from CONFIG_FILE, or fall back to the default."""
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE) as f:
raw = json.load(f)
return {int(k): v for k, v in raw.items()}
return dict(NOTE_MAP)
def save_note_map(note_map: dict):
with open(CONFIG_FILE, "w") as f:
json.dump({str(k): v for k, v in note_map.items()}, f, indent=2)
print(f"Saved note mapping to {CONFIG_FILE}")
def configure_interactive():
"""Ask the user, one dot position at a time, which note they want, then save it."""
print("Let's set up your cipher key.")
print("Braille dot layout:\n 1 4\n 2 5\n 3 6\n")
print("Enter a note for each position, e.g. C4, F#5, A3.")
print("Press Enter with no input to keep the current note shown in [brackets].\n")
current = load_note_map()
new_map = {}
used_notes = set()
for pos in range(1, 7):
default = current.get(pos, NOTE_MAP.get(pos))
while True:
raw = input(f"Position {pos} [{default}]: ").strip()
note = raw if raw else default
if not is_valid_note(note):
print(f" '{note}' isn't a valid note name (try e.g. C4, F#5, A3).")
continue
# normalize casing, e.g. "c#4" -> "C#4"
note = note[0].upper() + note[1:]
if note in used_notes:
print(f" '{note}' is already used for another position -- "
f"each position needs a distinct note so chords decode correctly.")
continue
used_notes.add(note)
new_map[pos] = note
break
apply_note_map(new_map)
save_note_map(new_map)
print("\nYour cipher key:")
for pos in range(1, 7):
print(f" {pos}: {new_map[pos]}")
# ---------------------------------------------------------------------------
# Core encode / decode logic
# ---------------------------------------------------------------------------
def text_to_braille(text: str):
"""text -> list of dot-tuples, one per character (unsupported chars skipped)."""
patterns = []
for ch in text.lower():
if ch in BRAILLE_ALPHABET:
patterns.append(BRAILLE_ALPHABET[ch])
else:
# Unsupported character (digit/punctuation) - skip with a warning.
print(f"[warning] skipping unsupported character: {ch!r}", file=sys.stderr)
return patterns
def braille_to_chords(patterns):
"""list of dot-tuples -> list of chords (each chord = list of note names)."""
chords = []
for dots in patterns:
chord = [NOTE_MAP[d] for d in dots]
chords.append(chord)
return chords
def chords_to_braille(chords):
"""list of chords -> list of dot-tuples."""
patterns = []
for chord in chords:
dots = tuple(sorted(NOTE_TO_DOT[note] for note in chord))
patterns.append(dots)
return patterns
def braille_to_text(patterns):
"""list of dot-tuples -> text string."""
letters = []
for dots in patterns:
letter = DOTS_TO_LETTER.get(frozenset(dots))
letters.append(letter if letter is not None else "?")
return "".join(letters)
def encode_message(text: str):
"""Full pipeline: text -> chords."""
return braille_to_chords(text_to_braille(text))
def decode_message(chords):
"""Full pipeline: chords -> text."""
return braille_to_text(chords_to_braille(chords))
# ---------------------------------------------------------------------------
# Formatting helpers for reading/writing chords as plain text
# ---------------------------------------------------------------------------
def chords_to_string(chords) -> str:
"""[['C5','A4'], ['C4']] -> 'C5+A4 | C4' (rest / space becomes '.')"""
parts = []
for chord in chords:
parts.append("+".join(chord) if chord else ".")
return " | ".join(parts)
def string_to_chords(s: str):
"""'C5+A4 | C4 | .' -> [['C5','A4'], ['C4'], []]"""
chords = []
for part in s.split("|"):
part = part.strip()
if part == "." or part == "":
chords.append([])
else:
chords.append([n.strip() for n in part.split("+")])
return chords
# ---------------------------------------------------------------------------
# Optional: export the encoded message as a real, playable MIDI file
# ---------------------------------------------------------------------------
def export_midi(chords, filename: str, beats_per_chord: float = 1.0, tempo: int = 100):
try:
from midiutil import MIDIFile
except ImportError:
print("midiutil not installed. Run: pip install midiutil --break-system-packages",
file=sys.stderr)
return
track, channel, volume = 0, 0, 100
midi = MIDIFile(1)
midi.addTempo(track, 0, tempo)
time = 0.0
for chord in chords:
for note_name in chord:
pitch = note_name_to_midi(note_name)
midi.addNote(track, channel, pitch, time, beats_per_chord, volume)
time += beats_per_chord
with open(filename, "wb") as f:
midi.writeFile(f)
print(f"MIDI written to {filename}")
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(description="Braille <-> Music cipher")
parser.add_argument("mode", choices=["encode", "decode", "configure"])
parser.add_argument("message", nargs="?", default=None,
help="text to encode, or chord string to decode "
"(not needed for 'configure')")
parser.add_argument("--midi", metavar="FILE", help="also write a .mid file (encode mode only)")
args = parser.parse_args()
if args.mode == "configure":
configure_interactive()
return
# For encode/decode, use a previously saved custom note map if one exists,
# otherwise fall back to the built-in default NOTE_MAP.
apply_note_map(load_note_map())
if args.message is None:
parser.error("the 'message' argument is required for encode/decode")
if args.mode == "encode":
chords = encode_message(args.message)
print(chords_to_string(chords))
if args.midi:
export_midi(chords, args.midi)
else:
chords = string_to_chords(args.message)
print(decode_message(chords))
if __name__ == "__main__":
main()