#!/usr/bin/env python3 """ Bell Melody Converter Converts human-readable bell notation to binary .bsm files for ESP32 Format: MELODY_NAME: 1,2,3+4,0,5+6+7 Output: MELODY_NAME.bsm (binary file, uint16_t big-endian) """ import sys import re from pathlib import Path from typing import List, Tuple def parse_bell_notation(notation: str) -> int: """ Convert human-readable bell notation to bit flag value Examples: "2+8" → bells 2,8 → bits 1,7 → 0x0082 (130) "4" → bell 4 → bit 3 → 0x0008 (8) "1+2+3" → bells 1,2,3 → bits 0,1,2 → 0x0007 (7) "0" → no bells → 0x0000 (0) Formula: Bell #N → Bit (N-1) → Value = 1 << (N-1) """ notation = notation.strip() # Handle zero/silence if notation == '0' or not notation: return 0 # Split by + to get individual bell numbers bell_numbers = notation.split('+') value = 0 for bell_str in bell_numbers: try: bell_num = int(bell_str.strip()) if bell_num == 0: continue # Bell 0 means silence, contributes nothing if bell_num < 1 or bell_num > 16: print(f"Warning: Bell number {bell_num} out of range (1-16), skipping") continue # Convert bell number to bit position (1-indexed to 0-indexed) bit_position = bell_num - 1 bit_value = 1 << bit_position value |= bit_value except ValueError: print(f"Warning: Invalid bell number '{bell_str}', skipping") continue return value def parse_melody_line(line: str) -> Tuple[str, List[int]]: """ Parse a melody line in format: MELODY_NAME: step,step,step Returns: (melody_name, list of uint16_t values) """ line = line.strip() if not line or line.startswith('#'): return None, [] # Split by colon if ':' not in line: print(f"Warning: Invalid format (missing ':'): {line}") return None, [] parts = line.split(':', 1) melody_name = parts[0].strip() steps_str = parts[1].strip() if not melody_name: print(f"Warning: Empty melody name in line: {line}") return None, [] # Parse steps (comma-separated) step_strings = steps_str.split(',') values = [] for i, step_str in enumerate(step_strings): value = parse_bell_notation(step_str) values.append(value) return melody_name, values def write_binary_melody(filepath: str, values: List[int]): """ Write melody values as binary file (uint16_t, big-endian) Args: filepath: Output file path values: List of uint16_t values (0-65535) """ with open(filepath, 'wb') as f: for value in values: # Ensure value fits in uint16_t if value > 0xFFFF: print(f"Warning: Value {value} exceeds uint16_t range, truncating") value = value & 0xFFFF # Write as 2 bytes, big-endian (MSB first) f.write(value.to_bytes(2, byteorder='big')) def convert_melodies_file(input_path: str, output_dir: str = '.'): """ Convert multi-melody file to individual .bsm binary files Args: input_path: Path to input text file output_dir: Directory for output .bsm files """ output_path = Path(output_dir) output_path.mkdir(parents=True, exist_ok=True) melodies_created = 0 total_steps = 0 try: with open(input_path, 'r', encoding='utf-8') as f: lines = f.readlines() print(f"Reading from: {input_path}") print(f"Output directory: {output_path.absolute()}\n") for line_num, line in enumerate(lines, 1): melody_name, values = parse_melody_line(line) if melody_name and values: # Create output filename output_file = output_path / f"{melody_name}.bsm" # Write binary file write_binary_melody(str(output_file), values) # Calculate file size file_size = len(values) * 2 # 2 bytes per uint16_t # Show what bells are used all_bells = set() for value in values: for bit in range(16): if value & (1 << bit): all_bells.add(bit + 1) # Convert back to 1-indexed bells_str = ','.join(map(str, sorted(all_bells))) if all_bells else 'none' print(f"✓ {melody_name}.bsm") print(f" Steps: {len(values)}") print(f" Size: {file_size} bytes") print(f" Bells used: {bells_str}") print() melodies_created += 1 total_steps += len(values) print(f"{'='*50}") print(f"✓ Successfully created {melodies_created} melody files") print(f" Total steps: {total_steps}") print(f" Total size: {total_steps * 2} bytes") return True except FileNotFoundError: print(f"Error: Input file '{input_path}' not found") return False except Exception as e: print(f"Error: {e}") import traceback traceback.print_exc() return False def main(): """Main entry point""" print("=== Bell Melody Converter ===") print("Creates binary .bsm files for ESP32\n") # Default input file input_file = "all_melodies.txt" output_dir = "." # Check if file exists if not Path(input_file).exists(): print(f"Error: '{input_file}' not found in current directory!") print("\nPlease create 'all_melodies.txt' with format:") print(" MELODY_NAME: step,step,step,...") print("\nStep notation:") print(" 0 - Silence") print(" 4 - Bell #4 only") print(" 2+8 - Bells #2 and #8 together") print(" 1+2+3 - Bells #1, #2, and #3 together") print("\nExample:") print(" JINGLE_BELLS: 4,4,4,0,4,4,4,0,4,8,1,2,4") print(" ALARM: 2+8,0,2+8,0,2+8,0") print(" HAPPY_BIRTHDAY: 1,1,2,1,4,3,0,1,1,2,1,8,4") sys.exit(1) success = convert_melodies_file(input_file, output_dir) sys.exit(0 if success else 1) if __name__ == "__main__": main()