Skip to content

Commit a2dff82

Browse files
committed
speaker: play exact frequency in speaker_play_tone()
The applib speaker_play_tone() wrapper used to round its frequency_hz argument to the nearest MIDI semitone before submitting a one-note sequence — a 400 Hz request would actually play as G4 (392 Hz). Add a dedicated tone path through the service and a sys_speaker_play_tone syscall that compute phase_inc directly from the frequency, and rewire the applib wrapper through it. The note sequencer keeps its MIDI-only identity for melodies and tracks; tones get their own primitive. Bump SDK revision to 98 (minor 0x5f) to mark the behavior change. Signed-off-by: Joshua Jun <lets@throw.rocks>
1 parent 8098801 commit a2dff82

7 files changed

Lines changed: 116 additions & 65 deletions

File tree

include/pbl/services/speaker/speaker_service.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ typedef enum {
2929
SpeakerSourceNoteSeq,
3030
SpeakerSourceStream,
3131
SpeakerSourceTracks,
32+
SpeakerSourceTone,
3233
} SpeakerSourceType;
3334

3435
//! Initialize the speaker service. Called once at boot.
@@ -43,6 +44,18 @@ void speaker_service_init(void);
4344
bool speaker_service_play_note_seq(const SpeakerNote *notes, uint32_t num_notes,
4445
SpeakerPriority pri, uint8_t vol);
4546

47+
//! Play a single tone at an exact frequency on the speaker.
48+
//! @param freq_hz Tone frequency in Hz (0 = silence/rest)
49+
//! @param duration_ms Tone duration in milliseconds (max 10000)
50+
//! @param waveform Waveform to use (SpeakerWaveform value)
51+
//! @param velocity Per-note amplitude scale 0-127 (0 = use master volume)
52+
//! @param pri Priority level
53+
//! @param vol Volume (0-100)
54+
//! @return true if playback started, false if preempted by higher priority
55+
bool speaker_service_play_tone(uint16_t freq_hz, uint16_t duration_ms,
56+
uint8_t waveform, uint8_t velocity,
57+
SpeakerPriority pri, uint8_t vol);
58+
4659
//! Play N monophonic tracks in parallel, mixed together.
4760
//! Track arrays and any sample data are copied into kernel memory.
4861
//! @param tracks Array of tracks. For each, its notes array and (optional)

src/fw/applib/ui/speaker.c

Lines changed: 3 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -9,60 +9,6 @@
99
#include "syscall/syscall.h"
1010
#include "system/logging.h"
1111

12-
// MIDI note number for a given frequency (approximate, nearest semitone)
13-
// Uses: midi = 69 + 12 * log2(freq/440)
14-
static uint8_t prv_freq_to_midi(uint16_t freq_hz) {
15-
if (freq_hz == 0) {
16-
return 0;
17-
}
18-
19-
// Simple lookup for common frequencies, otherwise approximate
20-
// Using integer math: find closest MIDI note
21-
// Standard: A4 = 440Hz = MIDI 69
22-
// Each semitone is freq * 2^(1/12) ~= freq * 1.0595
23-
24-
// Binary search approach: start from A4 and adjust
25-
int16_t note = 69;
26-
uint32_t target = (uint32_t)freq_hz * 256; // 8.8 fixed point
27-
28-
// Find the right octave and semitone using ratio comparison
29-
// Semitone ratios * 1024 relative to octave base
30-
static const uint16_t semitone_ratio_x1024[] = {
31-
1024, 1085, 1149, 1217, 1290, 1366, 1448, 1534, 1625, 1722, 1824, 1933
32-
};
33-
34-
// Find octave
35-
uint32_t base_freq = 440 * 256; // A4 in 8.8
36-
int octave_offset = 0;
37-
38-
while (target >= base_freq * 2 && note < 127) {
39-
base_freq *= 2;
40-
octave_offset++;
41-
}
42-
while (target < base_freq && note > 0) {
43-
base_freq /= 2;
44-
octave_offset--;
45-
}
46-
47-
// Find semitone within octave
48-
int best_semi = 0;
49-
uint32_t best_diff = UINT32_MAX;
50-
for (int s = 0; s < 12; s++) {
51-
uint32_t semi_freq = (base_freq * semitone_ratio_x1024[s]) / 1024;
52-
uint32_t diff = (target > semi_freq) ? (target - semi_freq) : (semi_freq - target);
53-
if (diff < best_diff) {
54-
best_diff = diff;
55-
best_semi = s;
56-
}
57-
}
58-
59-
note = 69 + octave_offset * 12 + best_semi;
60-
if (note < 0) note = 0;
61-
if (note > 127) note = 127;
62-
63-
return (uint8_t)note;
64-
}
65-
6612
bool speaker_play_notes(const SpeakerNote *notes, uint32_t num_notes, uint8_t volume) {
6713
if (!notes || num_notes == 0) {
6814
PBL_LOG_ERR("tried to play null or empty note sequence");
@@ -78,15 +24,9 @@ bool speaker_play_tone(uint16_t frequency_hz, uint32_t duration_ms,
7824
duration_ms = 10000;
7925
}
8026

81-
SpeakerNote note = {
82-
.midi_note = prv_freq_to_midi(frequency_hz),
83-
.waveform = (uint8_t)waveform,
84-
.duration_ms = (uint16_t)duration_ms,
85-
.velocity = 0, // use global volume
86-
.reserved = 0,
87-
};
88-
89-
return sys_speaker_play_note_seq(&note, 1, 0 /* SpeakerPriorityApp */, volume);
27+
return sys_speaker_play_tone(frequency_hz, (uint16_t)duration_ms,
28+
(uint8_t)waveform, 0 /* use global volume */,
29+
0 /* SpeakerPriorityApp */, volume);
9030
}
9131

9232
bool speaker_stream_open(SpeakerPcmFormat format, uint8_t volume) {

src/fw/process_management/pebble_process_info.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,10 @@ typedef enum {
160160
// sdk.major:0x5 .minor:0x5c -- Add light_is_on() and expose touch_service_subscribe/unsubscribe to apps (rev 95)
161161
// sdk.major:0x5 .minor:0x5d -- Add app_light_set_color_rgb888() for 8-bit-per-channel backlight tint (rev 96)
162162
// sdk.major:0x5 .minor:0x5e -- Add Speaker API (rev 97)
163+
// sdk.major:0x5 .minor:0x5f -- speaker_play_tone() now plays the exact frequency (rev 98)
163164

164165
#define PROCESS_INFO_CURRENT_SDK_VERSION_MAJOR 0x5
165-
#define PROCESS_INFO_CURRENT_SDK_VERSION_MINOR 0x5e
166+
#define PROCESS_INFO_CURRENT_SDK_VERSION_MINOR 0x5f
166167

167168
// The first SDK to ship with 2.x APIs
168169
#define PROCESS_INFO_FIRST_2X_SDK_VERSION_MAJOR 0x4

src/fw/services/speaker/speaker_service.c

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ typedef struct {
3535
NoteSequenceState note_seq;
3636
SpeakerNote *note_buf; // kernel_malloc'd copy of notes
3737

38+
// Single tone source (raw frequency, no MIDI quantization)
39+
uint32_t tone_samples_remaining;
40+
uint32_t tone_phase_acc; // 16.16 fixed-point
41+
uint32_t tone_phase_inc; // per-sample phase increment
42+
uint8_t tone_waveform;
43+
uint8_t tone_velocity;
44+
3845
// PCM stream source
3946
PcmStreamState pcm_stream;
4047
SpeakerPcmFormat pcm_format;
@@ -174,6 +181,9 @@ static void prv_stop_internal(SpeakerFinishReason reason) {
174181
pcm_stream_deinit(&s_state.pcm_stream);
175182
} else if (s_state.source_type == SpeakerSourceTracks) {
176183
prv_free_tracks();
184+
} else if (s_state.source_type == SpeakerSourceTone) {
185+
s_state.tone_samples_remaining = 0;
186+
s_state.tone_phase_inc = 0;
177187
}
178188

179189
s_state.state = SpeakerStateIdle;
@@ -330,6 +340,27 @@ static void prv_refill_bg(void *data) {
330340
memset(s_state.refill_buf, 0, SPEAKER_REFILL_SAMPLES * sizeof(int16_t));
331341
samples_generated = SPEAKER_REFILL_SAMPLES;
332342
}
343+
} else if (s_state.source_type == SpeakerSourceTone) {
344+
uint32_t to_gen = s_state.tone_samples_remaining;
345+
if (to_gen > SPEAKER_REFILL_SAMPLES) {
346+
to_gen = SPEAKER_REFILL_SAMPLES;
347+
}
348+
if (to_gen == 0) {
349+
prv_stop_internal(SpeakerFinishReasonDone);
350+
return;
351+
}
352+
if (s_state.tone_phase_inc == 0) {
353+
memset(s_state.refill_buf, 0, to_gen * sizeof(int16_t));
354+
} else {
355+
for (uint32_t i = 0; i < to_gen; i++) {
356+
s_state.refill_buf[i] = note_synth_sample(s_state.tone_waveform,
357+
s_state.tone_phase_acc,
358+
s_state.tone_velocity);
359+
s_state.tone_phase_acc += s_state.tone_phase_inc;
360+
}
361+
}
362+
s_state.tone_samples_remaining -= to_gen;
363+
samples_generated = to_gen;
333364
} else if (s_state.source_type == SpeakerSourceTracks) {
334365
memset(s_state.mix_buf, 0, sizeof(int32_t) * SPEAKER_REFILL_SAMPLES);
335366
uint32_t max_generated = 0;
@@ -401,6 +432,41 @@ bool speaker_service_play_note_seq(const SpeakerNote *notes, uint32_t num_notes,
401432
return true;
402433
}
403434

435+
bool speaker_service_play_tone(uint16_t freq_hz, uint16_t duration_ms,
436+
uint8_t waveform, uint8_t velocity,
437+
SpeakerPriority pri, uint8_t vol) {
438+
if (!s_state.initialized || duration_ms == 0) {
439+
return false;
440+
}
441+
442+
if (!prv_can_preempt(pri)) {
443+
return false;
444+
}
445+
446+
if (s_state.state != SpeakerStateIdle) {
447+
prv_stop_internal(SpeakerFinishReasonPreempted);
448+
}
449+
450+
s_state.tone_samples_remaining =
451+
((uint32_t)duration_ms * SPEAKER_SAMPLE_RATE) / 1000;
452+
s_state.tone_phase_acc = 0;
453+
// phase_inc = freq_hz * 65536 / sample_rate (16.16 fixed-point per sample)
454+
s_state.tone_phase_inc = (freq_hz != 0)
455+
? ((uint32_t)freq_hz * 65536u) / SPEAKER_SAMPLE_RATE : 0;
456+
s_state.tone_waveform = waveform;
457+
s_state.tone_velocity = velocity;
458+
459+
s_state.state = SpeakerStatePlaying;
460+
s_state.source_type = SpeakerSourceTone;
461+
s_state.priority = pri;
462+
s_state.volume = vol;
463+
464+
prv_start_audio(vol);
465+
prv_refill_bg(NULL);
466+
467+
return true;
468+
}
469+
404470
bool speaker_service_play_tracks(const SpeakerTrack *tracks, uint32_t num_tracks,
405471
SpeakerPriority pri, uint8_t vol) {
406472
if (!s_state.initialized || !tracks || num_tracks == 0 ||
@@ -611,6 +677,12 @@ bool speaker_service_play_note_seq(const SpeakerNote *notes, uint32_t num_notes,
611677
return false;
612678
}
613679

680+
bool speaker_service_play_tone(uint16_t freq_hz, uint16_t duration_ms,
681+
uint8_t waveform, uint8_t velocity,
682+
SpeakerPriority pri, uint8_t vol) {
683+
return false;
684+
}
685+
614686
bool speaker_service_play_tracks(const SpeakerTrack *tracks, uint32_t num_tracks,
615687
SpeakerPriority pri, uint8_t vol) {
616688
return false;

src/fw/syscall/syscall.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ int32_t sys_vibe_get_vibe_strength(void);
9494
#include "pbl/services/speaker/track.h"
9595
bool sys_speaker_play_note_seq(const SpeakerNote *notes, uint32_t num_notes,
9696
uint8_t priority, uint8_t volume);
97+
bool sys_speaker_play_tone(uint16_t freq_hz, uint16_t duration_ms,
98+
uint8_t waveform, uint8_t velocity,
99+
uint8_t priority, uint8_t volume);
97100
bool sys_speaker_play_tracks(const SpeakerTrack *tracks, uint32_t num_tracks,
98101
uint8_t priority, uint8_t volume);
99102
bool sys_speaker_stream_open(uint8_t priority, uint8_t volume, uint8_t format);

src/fw/syscall/syscall_speaker.c

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,28 @@ DEFINE_SYSCALL(bool, sys_speaker_play_note_seq, const SpeakerNote *notes,
3535
(SpeakerPriority)priority, volume);
3636
}
3737

38+
DEFINE_SYSCALL(bool, sys_speaker_play_tone, uint16_t freq_hz,
39+
uint16_t duration_ms, uint8_t waveform, uint8_t velocity,
40+
uint8_t priority, uint8_t volume) {
41+
if (priority > SpeakerPriorityCritical) {
42+
priority = SpeakerPriorityApp;
43+
}
44+
45+
if (waveform >= SpeakerWaveformCount) {
46+
syscall_failed();
47+
}
48+
49+
if (velocity > 127) {
50+
syscall_failed();
51+
}
52+
53+
PebbleTask task = pebble_task_get_current();
54+
speaker_service_set_owner_task(task);
55+
56+
return speaker_service_play_tone(freq_hz, duration_ms, waveform, velocity,
57+
(SpeakerPriority)priority, volume);
58+
}
59+
3860
DEFINE_SYSCALL(bool, sys_speaker_play_tracks, const SpeakerTrack *tracks,
3961
uint32_t num_tracks, uint8_t priority, uint8_t volume) {
4062
if (PRIVILEGE_WAS_ELEVATED) {

tools/generate_native_sdk/exported_symbols.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"You should also make sure you are obeying our API design guidelines:",
55
"https://pebbletechnology.atlassian.net/wiki/display/DEV/SDK+API+Design+Guidelines"
66
],
7-
"revision" : "97",
7+
"revision" : "98",
88
"version" : "2.0",
99
"files": [
1010
"fw/drivers/ambient_light.h",

0 commit comments

Comments
 (0)