SatCat5
codec_rtttl.cc
1 // Copyright 2025 The Aerospace Corporation.
3 // This file is a part of SatCat5, licensed under CERN-OHL-W v2 or later.
5 
6 #include <satcat5/codec_rtttl.h>
7 #include <satcat5/io_readable.h>
8 #include <satcat5/io_writeable.h>
9 #include <satcat5/utils.h>
10 
15 
16 // Convert BPM to whole-note duration, in milliseconds.
17 constexpr u32 bpm2msec(unsigned bpm) {
18  return u32(60000 / bpm);
19 }
20 
21 // Convert musical note (ABCDEFGH) to the offset within an active.
22 inline optional<int> char2note(char ch) {
23  optional<int> result;
24  if (ch == 'c' || ch == 'C') result = 0; // Octave starts with 'C'.
25  if (ch == 'd' || ch == 'D') result = 2;
26  if (ch == 'e' || ch == 'E') result = 4;
27  if (ch == 'f' || ch == 'F') result = 5;
28  if (ch == 'g' || ch == 'G') result = 7;
29  if (ch == 'a' || ch == 'A') result = 9;
30  if (ch == 'b' || ch == 'B') result = 11; // American notation
31  if (ch == 'h' || ch == 'H') result = 11; // European notation
32  if (ch == 'p' || ch == 'P') result = -1; // Rest / pause
33  return result;
34 }
35 
36 // Convert musical note to a fixed-point frequency scaling factor.
37 // (Factor is 2^16 times its frequency in Hz, or zero for silence.)
38 inline u32 note2freq(unsigned octave, int note) {
39  // Table spans one octave: A4 = 440 Hz -> C0 = 16.35 Hz = 1071618 LSBs.
40  static const unsigned TABLE[] = {
41  1071618, 1135340, 1202851, 1274376, 1350154, 1430439,
42  1515497, 1605613, 1701088, 1802240, 1909407, 2022946};
43  if (note < 0 || note > 11) return 0;
44  return TABLE[note] << octave;
45 }
46 
47 // Thin wrapper for a null-terminated string input.
48 bool RtttlDecoder::play(const char* src) {
49  satcat5::io::ArrayRead rd(src, strlen(src));
50  return play(&rd);
51 }
52 
53 // Reference: Two informal specifications of the RTTTL format.
54 // http://merwin.bespin.org/t4a/specs/nokia_rtttl.txt
55 // https://www.mobilefish.com/tutorials/rtttl/rtttl_quickguide_specification.html
56 // Note: This parser does not perform validation, but it has been written
57 // defensively to avoid side-effects beyond data written to "m_spkr".
59  // Abort if there's already a song in the queue.
60  if (m_queue.get_read_ready()) return false;
61 
62  // Set internal callback for deferred playback.
63  // (Constructor is constexpr, so it's easier to do this now.)
64  m_queue.set_callback(this);
65 
66  // Discard the "name" section:
67  while (src->get_read_ready()) {
68  if (src->read_u8() == ':') break;
69  }
70 
71  // Read and decode the default-value section.
72  // Notes with no duration use the default duration.
73  // Notes with no octave use the default octave (4/5/6/7).
74  // Beats-per-minute (BPM) sets the duration of a whole note.
75  m_duration = 4;
76  m_octave = 6;
77  m_whole_note = bpm2msec(63);
78  u32 accum = 0, index = 0;
79  char varname = 0;
80  while (src->get_read_ready()) {
81  // Each segment looks like "o=4," ending in ',' or ':'.
82  char ch = char(src->read_u8());
83  if (ch == ',' || ch == ':') {
84  // Store the variable we just parsed.
85  if (varname == 'd') m_duration = accum;
86  if (varname == 'o') m_octave = accum;
87  if (varname == 'b') m_whole_note = bpm2msec(accum);
88  // Reset parser state for next variable.
89  accum = 0; index = 0;
90  // End of section?
91  if (ch == ':') break;
92  } else if (ch == ' ' || ch == '\t') {
93  // Ignore whitespace.
94  } else if (++index == 1) {
95  // First character is the variable name.
96  varname = ch;
97  } else if ('0' <= ch && ch <= '9') {
98  // Parse decimal value.
99  accum = 10*accum + u32(ch - '0');
100  }
101  }
102 
103  // Parse individual notes until the speaker command queue is full.
104  // If there's more, copy it to the internal buffer. (See data_rcvd.)
105  while (read_note(src)) {}
106  bool done = !src->get_read_ready();
107  return m_spkr->write_finalize()
108  && (done || src->copy_and_finalize(&m_queue));
109 }
110 
112  // RTTTL data is more compact than the unpacked speaker commands,
113  // so parse more notes to keep the speaker's working buffer full.
114  unsigned count = 0;
115  while (read_note(src)) {++count;}
116  if (count) m_spkr->write_finalize();
117 }
118 
119 bool RtttlDecoder::read_note(Readable* src) {
120  // Are we able to proceed with the next note?
121  if (src->get_read_ready() == 0) return false;
122  if (m_spkr->get_write_space() < 12) return false;
123 
124  // Read and decode one note from the comma-delimited list.
125  // e.g., "32p,a,a,4a,a,a,4a,a,c6,f.,16g,2a,a#,a#,a#.,16a#"
126  // Each command consists of [duration] note [scale] [dot]:
127  // duration = Optional duration. "4" = Quarter note (1/4) etc.
128  // note = Offset within each octave 'a', 'a#', 'b', etc.
129  // (Sharp notes indicated by '#', no flats.)
130  // scale = Optional octave number (4/5/6/7)
131  // dot = Optional '.' indicating 1.5x duration.
132  u32 duration = m_duration; // Default duration, may override later.
133  u32 dot = 2; // Dot factor = 2/2 or 3/2.
134  int note = -1; // Offset within octave, or -1 for pause.
135  u32 accum = 0; // Accumulator for ASCII numbers.
136  while (src->get_read_ready()) {
137  // Each command ends in a comma or end-of-input.
138  char ch = char(src->read_u8());
139  if (ch == ',') {
140  break;
141  } else if ('0' <= ch && ch <= '9') {
142  // Parse decimal value.
143  accum = 10*accum + unsigned(ch - '0');
144  } else if (ch == '#') {
145  // Offset sharp notes by +1.
146  ++note;
147  } else if (ch == '.') {
148  // Enable 1.5x duration factor.
149  dot = 3;
150  } else if (char2note(ch).has_value()) {
151  // Store note value (ABCDEFGH or P) and duration, if present.
152  note = char2note(ch).value();
153  if (accum) duration = accum;
154  accum = 0;
155  }
156  }
157 
158  // Calculate duration and frequency.
159  u32 octave = accum ? accum : m_octave;
160  u16 msec = u16((m_whole_note * dot) / (2 * duration));
161  u64 freq = note2freq(octave, note);
162  if (freq) {
163  // Leave a short gap between notes (15/16 on, 1/16 off)
164  // Mostly required for consecutive notes of same pitch.
165  static constexpr u64 HALF_LSB = (1ull << 31);
166  u32 rate = u32((m_scale * freq + HALF_LSB) >> 32);
167  u16 gap = msec / 16;
168  m_spkr->write_u16(msec - gap);
169  m_spkr->write_u32(rate);
170  m_spkr->write_u16(gap);
171  m_spkr->write_u32(0);
172  } else {
173  // No gap required for pauses.
174  m_spkr->write_u16(msec);
175  m_spkr->write_u32(0);
176  }
177  return true;
178 }
179 
180 static inline const char* beep_code(s8 val) {
181  // Choose a sequence based on log-message priority.
182  if (val >= satcat5::log::CRITICAL)
183  return "sos:d=16,o=6,b=100:f,f,f,p,8f,8f,8f,p,f,f,f";
184  else if (val >= satcat5::log::ERROR)
185  return "err:d=32,o=6,b=100:f,d,e,d";
186  else if (val >= satcat5::log::WARNING)
187  return "wrn:d=32,o=6,b=100:f,d,c";
188  else if (val >= satcat5::log::INFO)
189  return "inf:d=32,o=6,b=100:e,f";
190  else
191  return nullptr;
192 }
193 
194 ToBeep::ToBeep(satcat5::io::RtttlDecoder* codec)
195  : m_codec(codec), m_cooldown(500) {}
196 
197 void ToBeep::log_event(s8 priority, unsigned nbytes, const char* msg) {
198  // Ignore messages if we're disabled or still on cooldown.
199  if (timer_remaining() || !m_cooldown) return;
200 
201  // Otherwise, choose a beep-code and play if applicable.
202  const char* beep = beep_code(priority);
203  if (beep) {
204  m_codec->play(beep);
205  timer_once(m_cooldown);
206  }
207 }
Ephemeral Readable interface for a simple array.
Definition: io_readable.h:206
unsigned get_read_ready() const override
How many bytes can be read without blocking?
Definition: pkt_buffer.cc:177
Abstract API for reading byte-streams and packets.
Definition: io_readable.h:68
u8 read_u8()
One of many functions for reading integer/floating point values, see details.
Definition: io_readable.cc:44
bool copy_and_finalize(satcat5::io::Writeable *dst, satcat5::io::CopyMode mode=CopyMode::PACKET)
Copy data to a Writeable object, then finalize.
Definition: io_readable.cc:234
virtual void set_callback(satcat5::io::EventListener *callback)
Update registered callback for data_rcvd() events.
Definition: io_readable.cc:39
virtual unsigned get_read_ready() const =0
How many bytes can be read without blocking?
Ring Tone Text Transfer Language (RTTTL) interpreter.
Definition: codec_rtttl.h:33
u32 m_octave
Default octave.
Definition: codec_rtttl.h:67
u32 m_whole_note
Duration of whole note.
Definition: codec_rtttl.h:68
bool play(const char *src)
Decode and play the specified song (string input).
Definition: codec_rtttl.cc:48
satcat5::io::PacketBuffer m_queue
Playback queue.
Definition: codec_rtttl.h:69
u32 m_duration
Default note duration.
Definition: codec_rtttl.h:66
satcat5::io::Writeable *const m_spkr
Output device.
Definition: codec_rtttl.h:64
const u64 m_scale
Frequency conversion.
Definition: codec_rtttl.h:65
void data_rcvd(satcat5::io::Readable *src) override
The data_rcvd() callback is polled whenever data is available.
Definition: codec_rtttl.cc:111
virtual unsigned get_write_space() const =0
How many bytes can be written without blocking?
virtual bool write_finalize()
Mark end of frame and release temporary working data.
Respond to log messages by playing a few musical notes.
Definition: codec_rtttl.h:104
void log_event(s8 priority, unsigned nbytes, const char *msg) override
Callback for each formatted Log message.
Definition: codec_rtttl.cc:197
unsigned timer_remaining() const
Accessor for time to next event, if one is set.
Definition: polling.h:229
void timer_once(unsigned msec)
Configure a one-time notification after X milliseconds.
Definition: polling.cc:316
"Readable" I/O interface core definitions
"Writeable" I/O interface core definitions
An optional field that may be filled or empty.
Definition: utils.h:53
Miscellaneous mathematical utility functions.