miniDSP
A small C library for audio DSP
Loading...
Searching...
No Matches
minidsp_spectext.c
Go to the documentation of this file.
1
11
12#include "minidsp.h"
13#include "minidsp_internal.h"
14
15/* -----------------------------------------------------------------------
16 * 5x7 Bitmap Font (printable ASCII 32–126)
17 *
18 * Each character occupies 5 columns. Each column is one byte: bit 0 is
19 * the top row, bit 6 is the bottom row. Bits 7 is unused.
20 * -----------------------------------------------------------------------*/
21
22static const unsigned char font_5x7[95][5] = {
23 /* 32 ' ' */ {0x00, 0x00, 0x00, 0x00, 0x00},
24 /* 33 '!' */ {0x00, 0x00, 0x5F, 0x00, 0x00},
25 /* 34 '"' */ {0x00, 0x07, 0x00, 0x07, 0x00},
26 /* 35 '#' */ {0x14, 0x7F, 0x14, 0x7F, 0x14},
27 /* 36 '$' */ {0x24, 0x2A, 0x7F, 0x2A, 0x12},
28 /* 37 '%' */ {0x23, 0x13, 0x08, 0x64, 0x62},
29 /* 38 '&' */ {0x36, 0x49, 0x55, 0x22, 0x50},
30 /* 39 ''' */ {0x00, 0x05, 0x03, 0x00, 0x00},
31 /* 40 '(' */ {0x00, 0x1C, 0x22, 0x41, 0x00},
32 /* 41 ')' */ {0x00, 0x41, 0x22, 0x1C, 0x00},
33 /* 42 '*' */ {0x14, 0x08, 0x3E, 0x08, 0x14},
34 /* 43 '+' */ {0x08, 0x08, 0x3E, 0x08, 0x08},
35 /* 44 ',' */ {0x00, 0x50, 0x30, 0x00, 0x00},
36 /* 45 '-' */ {0x08, 0x08, 0x08, 0x08, 0x08},
37 /* 46 '.' */ {0x00, 0x60, 0x60, 0x00, 0x00},
38 /* 47 '/' */ {0x20, 0x10, 0x08, 0x04, 0x02},
39 /* 48 '0' */ {0x3E, 0x51, 0x49, 0x45, 0x3E},
40 /* 49 '1' */ {0x00, 0x42, 0x7F, 0x40, 0x00},
41 /* 50 '2' */ {0x42, 0x61, 0x51, 0x49, 0x46},
42 /* 51 '3' */ {0x21, 0x41, 0x45, 0x4B, 0x31},
43 /* 52 '4' */ {0x18, 0x14, 0x12, 0x7F, 0x10},
44 /* 53 '5' */ {0x27, 0x45, 0x45, 0x45, 0x39},
45 /* 54 '6' */ {0x3C, 0x4A, 0x49, 0x49, 0x30},
46 /* 55 '7' */ {0x01, 0x71, 0x09, 0x05, 0x03},
47 /* 56 '8' */ {0x36, 0x49, 0x49, 0x49, 0x36},
48 /* 57 '9' */ {0x06, 0x49, 0x49, 0x29, 0x1E},
49 /* 58 ':' */ {0x00, 0x36, 0x36, 0x00, 0x00},
50 /* 59 ';' */ {0x00, 0x56, 0x36, 0x00, 0x00},
51 /* 60 '<' */ {0x08, 0x14, 0x22, 0x41, 0x00},
52 /* 61 '=' */ {0x14, 0x14, 0x14, 0x14, 0x14},
53 /* 62 '>' */ {0x00, 0x41, 0x22, 0x14, 0x08},
54 /* 63 '?' */ {0x02, 0x01, 0x51, 0x09, 0x06},
55 /* 64 '@' */ {0x32, 0x49, 0x79, 0x41, 0x3E},
56 /* 65 'A' */ {0x7E, 0x11, 0x11, 0x11, 0x7E},
57 /* 66 'B' */ {0x7F, 0x49, 0x49, 0x49, 0x36},
58 /* 67 'C' */ {0x3E, 0x41, 0x41, 0x41, 0x22},
59 /* 68 'D' */ {0x7F, 0x41, 0x41, 0x22, 0x1C},
60 /* 69 'E' */ {0x7F, 0x49, 0x49, 0x49, 0x41},
61 /* 70 'F' */ {0x7F, 0x09, 0x09, 0x09, 0x01},
62 /* 71 'G' */ {0x3E, 0x41, 0x49, 0x49, 0x7A},
63 /* 72 'H' */ {0x7F, 0x08, 0x08, 0x08, 0x7F},
64 /* 73 'I' */ {0x00, 0x41, 0x7F, 0x41, 0x00},
65 /* 74 'J' */ {0x20, 0x40, 0x41, 0x3F, 0x01},
66 /* 75 'K' */ {0x7F, 0x08, 0x14, 0x22, 0x41},
67 /* 76 'L' */ {0x7F, 0x40, 0x40, 0x40, 0x40},
68 /* 77 'M' */ {0x7F, 0x02, 0x0C, 0x02, 0x7F},
69 /* 78 'N' */ {0x7F, 0x04, 0x08, 0x10, 0x7F},
70 /* 79 'O' */ {0x3E, 0x41, 0x41, 0x41, 0x3E},
71 /* 80 'P' */ {0x7F, 0x09, 0x09, 0x09, 0x06},
72 /* 81 'Q' */ {0x3E, 0x41, 0x51, 0x21, 0x5E},
73 /* 82 'R' */ {0x7F, 0x09, 0x19, 0x29, 0x46},
74 /* 83 'S' */ {0x46, 0x49, 0x49, 0x49, 0x31},
75 /* 84 'T' */ {0x01, 0x01, 0x7F, 0x01, 0x01},
76 /* 85 'U' */ {0x3F, 0x40, 0x40, 0x40, 0x3F},
77 /* 86 'V' */ {0x1F, 0x20, 0x40, 0x20, 0x1F},
78 /* 87 'W' */ {0x3F, 0x40, 0x38, 0x40, 0x3F},
79 /* 88 'X' */ {0x63, 0x14, 0x08, 0x14, 0x63},
80 /* 89 'Y' */ {0x07, 0x08, 0x70, 0x08, 0x07},
81 /* 90 'Z' */ {0x61, 0x51, 0x49, 0x45, 0x43},
82 /* 91 '[' */ {0x00, 0x7F, 0x41, 0x41, 0x00},
83 /* 92 '\' */ {0x02, 0x04, 0x08, 0x10, 0x20},
84 /* 93 ']' */ {0x00, 0x41, 0x41, 0x7F, 0x00},
85 /* 94 '^' */ {0x04, 0x02, 0x01, 0x02, 0x04},
86 /* 95 '_' */ {0x40, 0x40, 0x40, 0x40, 0x40},
87 /* 96 '`' */ {0x00, 0x01, 0x02, 0x04, 0x00},
88 /* 97 'a' */ {0x20, 0x54, 0x54, 0x54, 0x78},
89 /* 98 'b' */ {0x7F, 0x48, 0x44, 0x44, 0x38},
90 /* 99 'c' */ {0x38, 0x44, 0x44, 0x44, 0x20},
91 /*100 'd' */ {0x38, 0x44, 0x44, 0x48, 0x7F},
92 /*101 'e' */ {0x38, 0x54, 0x54, 0x54, 0x18},
93 /*102 'f' */ {0x08, 0x7E, 0x09, 0x01, 0x02},
94 /*103 'g' */ {0x0C, 0x52, 0x52, 0x52, 0x3E},
95 /*104 'h' */ {0x7F, 0x08, 0x04, 0x04, 0x78},
96 /*105 'i' */ {0x00, 0x44, 0x7D, 0x40, 0x00},
97 /*106 'j' */ {0x20, 0x40, 0x44, 0x3D, 0x00},
98 /*107 'k' */ {0x7F, 0x10, 0x28, 0x44, 0x00},
99 /*108 'l' */ {0x00, 0x41, 0x7F, 0x40, 0x00},
100 /*109 'm' */ {0x7C, 0x04, 0x18, 0x04, 0x78},
101 /*110 'n' */ {0x7C, 0x08, 0x04, 0x04, 0x78},
102 /*111 'o' */ {0x38, 0x44, 0x44, 0x44, 0x38},
103 /*112 'p' */ {0x7C, 0x14, 0x14, 0x14, 0x08},
104 /*113 'q' */ {0x08, 0x14, 0x14, 0x18, 0x7C},
105 /*114 'r' */ {0x7C, 0x08, 0x04, 0x04, 0x08},
106 /*115 's' */ {0x48, 0x54, 0x54, 0x54, 0x20},
107 /*116 't' */ {0x04, 0x3F, 0x44, 0x40, 0x20},
108 /*117 'u' */ {0x3C, 0x40, 0x40, 0x20, 0x7C},
109 /*118 'v' */ {0x1C, 0x20, 0x40, 0x20, 0x1C},
110 /*119 'w' */ {0x3C, 0x40, 0x30, 0x40, 0x3C},
111 /*120 'x' */ {0x44, 0x28, 0x10, 0x28, 0x44},
112 /*121 'y' */ {0x0C, 0x50, 0x50, 0x50, 0x3C},
113 /*122 'z' */ {0x44, 0x64, 0x54, 0x4C, 0x44},
114 /*123 '{' */ {0x00, 0x08, 0x36, 0x41, 0x00},
115 /*124 '|' */ {0x00, 0x00, 0x7F, 0x00, 0x00},
116 /*125 '}' */ {0x00, 0x41, 0x36, 0x08, 0x00},
117 /*126 '~' */ {0x10, 0x08, 0x08, 0x10, 0x10},
118};
119
123static int pixel_at(const char *text, unsigned len, unsigned col, unsigned row)
124{
125 unsigned char_width = 8; /* 5 data columns + 3 spacing columns */
126 unsigned char_idx = col / char_width;
127 unsigned col_in_char = col % char_width;
128
129 if (char_idx >= len) return 0;
130 if (col_in_char >= 5) return 0; /* spacing column */
131
132 char ch = text[char_idx];
133 if (ch < 32 || ch > 126) ch = '?'; /* replace unprintable */
134
135 unsigned glyph_idx = (unsigned)(ch - 32);
136 unsigned char col_byte = font_5x7[glyph_idx][col_in_char];
137 return (col_byte >> row) & 1;
138}
139
140/* -----------------------------------------------------------------------
141 * Public API: spectrogram text synthesis
142 * -----------------------------------------------------------------------*/
143
144unsigned MD_spectrogram_text(double *output, unsigned max_len,
145 const char *text,
146 double freq_lo, double freq_hi,
147 double duration_sec, double sample_rate)
148{
149 MD_CHECK(output != NULL, MD_ERR_NULL_POINTER, "output must not be NULL", 0);
150 MD_CHECK(text != NULL, MD_ERR_NULL_POINTER, "text must not be NULL", 0);
151 MD_CHECK(text[0] != '\0', MD_ERR_INVALID_SIZE, "text must not be empty", 0);
152 MD_CHECK(freq_lo > 0.0, MD_ERR_INVALID_RANGE, "freq_lo must be > 0", 0);
153 MD_CHECK(freq_lo < freq_hi, MD_ERR_INVALID_RANGE, "freq_lo must be < freq_hi", 0);
154 MD_CHECK(freq_hi < sample_rate / 2.0, MD_ERR_INVALID_RANGE,
155 "freq_hi must be < Nyquist", 0);
156 MD_CHECK(duration_sec > 0.0, MD_ERR_INVALID_RANGE, "duration_sec must be > 0", 0);
157 MD_CHECK(sample_rate > 0.0, MD_ERR_INVALID_RANGE, "sample_rate must be > 0", 0);
158
159 unsigned len = (unsigned)strlen(text);
160 unsigned grid_cols = len * 8 - 3; /* 5 data + 3 space per char, minus trailing spaces */
161 unsigned grid_rows = 7;
162
163 unsigned col_samples = (unsigned)(duration_sec / (double)grid_cols * sample_rate);
164 if (col_samples < 1) col_samples = 1;
165
166 unsigned total_samples = col_samples * grid_cols;
167 MD_CHECK(max_len >= total_samples, MD_ERR_INVALID_SIZE,
168 "max_len too small for total samples", 0);
169
170 /* Crossfade: 3 ms raised-cosine, clamped to half a column */
171 unsigned fade_samples = (unsigned)(0.003 * sample_rate);
172 if (fade_samples > col_samples / 2) fade_samples = col_samples / 2;
173
174 /* Per-row frequency: row 0 → freq_hi, row 6 → freq_lo */
175 double row_freq[7];
176 for (unsigned r = 0; r < grid_rows; r++) {
177 row_freq[r] = freq_hi - (double)r / (double)(grid_rows - 1)
178 * (freq_hi - freq_lo);
179 }
180
181 /* Per-row phase accumulator (for continuous tones) */
182 double phase[7] = {0};
183
184 /* Zero the output buffer */
185 memset(output, 0, total_samples * sizeof(double));
186
187 for (unsigned c = 0; c < grid_cols; c++) {
188 unsigned base = c * col_samples;
189
190 for (unsigned r = 0; r < grid_rows; r++) {
191 int on_now = pixel_at(text, len, c, r);
192 int on_prev = (c > 0) ? pixel_at(text, len, c - 1, r) : 0;
193 int on_next = (c + 1 < grid_cols) ? pixel_at(text, len, c + 1, r) : 0;
194
195 double dp = 2.0 * M_PI * row_freq[r] / sample_rate;
196
197 for (unsigned s = 0; s < col_samples; s++) {
198 if (!on_now) {
199 /* Advance phase even when off so re-entry is smooth */
200 phase[r] = fmod(phase[r] + dp, 2.0 * M_PI);
201 continue;
202 }
203
204 double envelope = 1.0;
205
206 /* Fade-in at column start if previous column was off */
207 if (!on_prev && s < fade_samples && fade_samples > 0) {
208 double t = (double)s / (double)fade_samples;
209 envelope *= 0.5 * (1.0 - cos(M_PI * t));
210 }
211
212 /* Fade-out at column end if next column is off */
213 if (!on_next && s >= col_samples - fade_samples && fade_samples > 0) {
214 double t = (double)(col_samples - 1 - s) / (double)fade_samples;
215 envelope *= 0.5 * (1.0 - cos(M_PI * t));
216 }
217
218 output[base + s] += envelope * sin(phase[r]);
219 phase[r] = fmod(phase[r] + dp, 2.0 * M_PI);
220 }
221 }
222 }
223
224 /* Normalize to 0.9 peak amplitude */
225 double peak = 0.0;
226 for (unsigned i = 0; i < total_samples; i++) {
227 double a = fabs(output[i]);
228 if (a > peak) peak = a;
229 }
230 if (peak > 0.0) {
231 double scale = 0.9 / peak;
232 for (unsigned i = 0; i < total_samples; i++) {
233 output[i] *= scale;
234 }
235 }
236
237 return total_samples;
238}
A mini library of DSP (Digital Signal Processing) routines.
@ MD_ERR_INVALID_SIZE
A size or count argument is invalid (e.g.
Definition minidsp.h:63
@ MD_ERR_INVALID_RANGE
A range or bound is invalid (e.g.
Definition minidsp.h:64
@ MD_ERR_NULL_POINTER
A required pointer argument is NULL.
Definition minidsp.h:62
Internal header for cross-file dependencies within the minidsp module.
#define MD_CHECK(cond, code, msg, retval)
Check a precondition in a function that returns a value.
static int pixel_at(const char *text, unsigned len, unsigned col, unsigned row)
Return the pixel state (0 or 1) at bitmap coordinate (col, row) for the given string.
unsigned MD_spectrogram_text(double *output, unsigned max_len, const char *text, double freq_lo, double freq_hi, double duration_sec, double sample_rate)
Synthesise audio that displays readable text in a spectrogram.