From 84840fe77fe7b13e6190c5a5f505e24c17b1dc6e Mon Sep 17 00:00:00 2001 From: Jiri Vlasak Date: Mon, 11 May 2026 09:09:12 +0200 Subject: [PATCH] audioutils: Add RTTTL parsing library Add a simple library for parsing Ring Tone Text Transfer Language (RTTTL). Signed-off-by: Jiri Vlasak --- audioutils/rtttl-c/.gitignore | 1 + audioutils/rtttl-c/Kconfig | 17 +++ audioutils/rtttl-c/Make.defs | 25 ++++ audioutils/rtttl-c/Makefile | 36 +++++ audioutils/rtttl-c/rtttl.c | 271 ++++++++++++++++++++++++++++++++++ audioutils/rtttl-c/rtttl.h | 58 ++++++++ 6 files changed, 408 insertions(+) create mode 100644 audioutils/rtttl-c/.gitignore create mode 100644 audioutils/rtttl-c/Kconfig create mode 100644 audioutils/rtttl-c/Make.defs create mode 100644 audioutils/rtttl-c/Makefile create mode 100644 audioutils/rtttl-c/rtttl.c create mode 100644 audioutils/rtttl-c/rtttl.h diff --git a/audioutils/rtttl-c/.gitignore b/audioutils/rtttl-c/.gitignore new file mode 100644 index 00000000000..554f9351380 --- /dev/null +++ b/audioutils/rtttl-c/.gitignore @@ -0,0 +1 @@ +/rtttl-c diff --git a/audioutils/rtttl-c/Kconfig b/audioutils/rtttl-c/Kconfig new file mode 100644 index 00000000000..8def7ca9c63 --- /dev/null +++ b/audioutils/rtttl-c/Kconfig @@ -0,0 +1,17 @@ +config AUDIOUTILS_RTTTL_C + bool "RTTTL parsing library" + default n + ---help--- + Simple library for parsing Ring Tone Text Transfer Language (RTTTL) + + Define what to do with a tone: + + void + play_tone(struct rtttl_tone tone) + { + /* TODO */ + } + + and then play RTTTL string: + + rtttl_play("...", play_tone); diff --git a/audioutils/rtttl-c/Make.defs b/audioutils/rtttl-c/Make.defs new file mode 100644 index 00000000000..fa9aa98f88c --- /dev/null +++ b/audioutils/rtttl-c/Make.defs @@ -0,0 +1,25 @@ +############################################################################ +# apps/audioutils/rtttl-c/Make.defs +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +ifneq ($(CONFIG_AUDIOUTILS_RTTTL_C),) +CONFIGURED_APPS += $(APPDIR)/audioutils/rtttl-c +endif diff --git a/audioutils/rtttl-c/Makefile b/audioutils/rtttl-c/Makefile new file mode 100644 index 00000000000..fdfa761d181 --- /dev/null +++ b/audioutils/rtttl-c/Makefile @@ -0,0 +1,36 @@ +############################################################################ +# apps/audioutils/rtttl-c/Makefile +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +include $(APPDIR)/Make.defs + +CSRCS = rtttl.c + +create_includes: rtttl.h + $(Q) cp $< $(APPDIR)/include/audioutils + +context:: + $(Q) $(MAKE) create_includes + +distclean:: + $(call DELFILE, $(APPDIR)/include/audioutils/rtttl.h) + +include $(APPDIR)/Application.mk diff --git a/audioutils/rtttl-c/rtttl.c b/audioutils/rtttl-c/rtttl.c new file mode 100644 index 00000000000..6f824e8ae42 --- /dev/null +++ b/audioutils/rtttl-c/rtttl.c @@ -0,0 +1,271 @@ +/* + * SPDX-FileCopyrightText: 2026 Jiri Vlasak + * + * SPDX-License-Identifier: Apache-2.0 + * SPDX-License-Identifier: MIT + */ + +#include "rtttl.h" + +static int +is_allowed_in_part1(char c) +{ + return ':' != c && ' ' <= c && c < 0x7f; +} + +static int +isnum(char c) +{ + return '0' <= c && c <= '9'; +} + +static int +getnum(char const **p) +{ + int num = (int)(**p - '0'); + (*p)++; + + while (isnum(**p)) { + num *= 10; + num += (int)(**p - '0'); + (*p)++; + } + + return num; +} + +static int +is_duration(int n) +{ + return 1 == n || 2 == n || 4 == n || 8 == n || 16 == n || 32 == n; +} + +static int +is_octave(int n) +{ + return 4 == n || 5 == n || 6 == n || 7 == n; +} + +static void +skip_spaces(char const **p) +{ + while (' ' == **p || '\t' == **p) { + (*p)++; + } +} + +struct rtttl_conf +rtttl_parse_conf(char const *rtttl_string) +{ + char const *p = rtttl_string; + int num; + struct rtttl_conf conf = { + .error = 0, + .duration = 4, + .octave = 6, + .beat = 63, + }; + + /* Skip part 1 */ + + while (is_allowed_in_part1(*p)) { + p++; + } + p++; + + /* Parse part 2 */ + + while (':' != *p && '\0' != *p) { + skip_spaces(&p); /* Not by specification, but hey. */ + + switch (*p) { + case 'd': + p++; + if ('=' != *p) { + conf.error = -2; + } + p++; + num = -1; + if (isnum(*p)) { + num = getnum(&p); + } else { + conf.error = -3; + } + if (is_duration(num)) { + conf.duration = num; + } else { + conf.error = -4; + } + break; + case 'o': + p++; + if ('=' != *p) { + conf.error = -2; + } + p++; + num = -1; + if (isnum(*p)) { + num = getnum(&p); + } else { + conf.error = -5; + } + if (is_octave(num)) { + conf.octave = num; + } else { + conf.error = -6; + } + break; + case 'b': + p++; + if ('=' != *p) { + conf.error = -2; + } + p++; + num = -1; + if (isnum(*p)) { + conf.beat = getnum(&p); + } else { + conf.error = -7; + } + break; + default: + conf.error = -2; + } + if (',' == *p) { + p++; + } + } + + return conf; +} + +static int +tone_frequency(char const **p) +{ + /* See https://muted.io/note-frequencies/ */ + + char tone = **p; + (*p)++; + int hash = **p == '#'; + + if (hash) { + (*p)++; + } + + switch (tone) { + case 'p': + case 'P': + return 0; + case 'c': + case 'C': + return hash ? 27718 : 26163; + case 'd': + case 'D': + return hash ? 31113 : 29366; + case 'e': + case 'E': + return hash ? 34923 : 32963; + case 'f': + case 'F': + return hash ? 36999 : 34923; + case 'g': + case 'G': + return hash ? 41530 : 39200; + case 'a': + case 'A': + return hash ? 46616 : 44000; + case 'b': + case 'B': + return hash ? 52325 : 49388; + } + return -8; +} + +int +rtttl_play(char const *rtttl_string, void (*make_tone)(struct rtttl_tone)) +{ + struct rtttl_conf conf = rtttl_parse_conf(rtttl_string); + if (0 != conf.error) { + return conf.error; + } + + int num; + struct rtttl_tone tone; + char const *p = rtttl_string; + + /* Skip part 1 */ + + while (is_allowed_in_part1(*p)) { + p++; + } + p++; + + /* Skip part 2 */ + + while (':' != *p && '\0' != *p) { + p++; + } + p++; + + /* Play part 3 */ + + while ('\0' != *p) { + skip_spaces(&p); /* Not by specification, but hey. */ + + tone.duration_us = 60 * 1000000; + tone.duration_us /= conf.beat; + tone.duration_us *= 4; /* Usually quarter notes per minute. */ + num = -1; + if (isnum(*p)) { + num = getnum(&p); + } + if (!is_duration(num)) { + num = conf.duration; + } + tone.duration_us /= num; + + tone.frequency_100hz = tone_frequency(&p); + if (0 > tone.frequency_100hz) { + return tone.frequency_100hz; + } + + /* People sometimes write dot here. Why? */ + if ('.' == *p) { + tone.duration_us >>= 1; + tone.duration_us *= 3; + p++; + } + + num = -1; + if (isnum(*p)) { + num = getnum(&p); + } + if (!is_octave(num)) { + num = conf.octave; + } + tone.frequency_100hz <<= (num - 4); /* Double frequency? */ + + /* Frequency is returned in *100 Hz due to scaling. */ + if (0 != tone.frequency_100hz) { + tone.period_us = 100 * 1000000 / tone.frequency_100hz; + } else { + tone.period_us = 0; /* Pause. */ + } + + /* People should put dot here if they want to. */ + if ('.' == *p) { + tone.duration_us >>= 1; + tone.duration_us *= 3; + p++; + } + + make_tone(tone); + + if (',' == *p) { + p++; + } else if ('\0' != *p) { + return -9; + } + } + return 0; +} diff --git a/audioutils/rtttl-c/rtttl.h b/audioutils/rtttl-c/rtttl.h new file mode 100644 index 00000000000..ee6036685ce --- /dev/null +++ b/audioutils/rtttl-c/rtttl.h @@ -0,0 +1,58 @@ +/****************************************************************************** +* Ring Tone Text Transfer Language * +******************************************************************************* +* * +* See https://en.wikipedia.org/wiki/Ring_Tone_Text_Transfer_Language * +* * +* Play (test) RTTTL Online at https://adamonsoon.github.io/rtttl-play/ * +* * +* RTTTL in BNF at http://merwin.bespin.org/t4a/specs/nokia_rtttl.txt * +* * +* Note Frequency Chart at https://muted.io/note-frequencies/ * +* * +* RTTTL format at https://www.srtware.com/ringtones/ * +* * +******************************************************************************* +******************************************************************************* +* * +* SPDX-FileCopyrightText: 2026 Jiri Vlasak * +* * +* SPDX-License-Identifier: Apache-2.0 * +* SPDX-License-Identifier: MIT * +* * +******************************************************************************/ + +#ifndef RTTTL_C_H +#define RTTTL_C_H + +struct rtttl_tone { + int frequency_100hz; /* Tone frequency ; 440 Hz for A4. */ + int period_us; /* 1 / frequency in microseconds. */ + int duration_us; /* How long to play in microseconds? */ +}; + +struct rtttl_conf { + int error; + /* Describes the result of rtttl_parse_conf procedure. + * 0 Good. + * -1 Invalid charatecter(s) in part 1. + * -2 Invalid syntax in part 2. + * -3 Default duration not number. + * -4 Default duration not in allowed range. + * -5 Default octave not number. + * -6 Default octave not in allowed range. + * -7 Default beat not number. + * -8 Bad note. + * -9 Invalid syntax in part 3. + */ + + int duration; /* How long to play? May be: 1 2 4 8 16 32 */ + int octave; /* What octave? May be: 4 5 6 7 */ + int beat; /* Beats per minute */ +}; + +struct rtttl_conf rtttl_parse_conf(char const *rtttl_string); + +int rtttl_play(char const *rtttl_string, void (*make_tone)(struct rtttl_tone)); + +#endif /* RTTTL_C_H */