1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
|
#include "study.h"
#include <stdio.h>
/* ------------------------------------------------------------------ */
/* Silence detection */
/* ------------------------------------------------------------------ */
void study_detect_silence(const char *path, PlayerState *state,
float threshold, float minDuration)
{
state->silenceCount = 0;
Wave wave = LoadWave(path);
if (wave.data == NULL || wave.frameCount == 0) return;
/* Convert to 32-bit float mono for easy analysis */
WaveFormat(&wave, wave.sampleRate, 32, 1);
float *samples = (float *)wave.data;
unsigned int totalFrames = wave.frameCount;
float sampleRate = (float)wave.sampleRate;
/* Scan in chunks of ~10ms */
int chunkSize = (int)(sampleRate * 0.01f);
if (chunkSize < 1) chunkSize = 1;
float minFrames = minDuration * sampleRate;
bool inSilence = false;
unsigned int silenceStart = 0;
for (unsigned int i = 0; i < totalFrames; i += chunkSize)
{
unsigned int end = i + chunkSize;
if (end > totalFrames) end = totalFrames;
/* Find peak amplitude in this chunk */
float peak = 0.0f;
for (unsigned int j = i; j < end; j++)
{
float v = samples[j];
if (v < 0) v = -v;
if (v > peak) peak = v;
}
if (peak < threshold)
{
if (!inSilence) { silenceStart = i; inSilence = true; }
}
else
{
if (inSilence)
{
unsigned int len = i - silenceStart;
if ((float)len >= minFrames && state->silenceCount < MAX_SILENCE_REGIONS)
{
state->silence[state->silenceCount].start = (float)silenceStart / (float)totalFrames;
state->silence[state->silenceCount].end = (float)i / (float)totalFrames;
state->silenceCount++;
}
inSilence = false;
}
}
}
/* Close any trailing silence */
if (inSilence)
{
unsigned int len = totalFrames - silenceStart;
if ((float)len >= minFrames && state->silenceCount < MAX_SILENCE_REGIONS)
{
state->silence[state->silenceCount].start = (float)silenceStart / (float)totalFrames;
state->silence[state->silenceCount].end = (float)totalFrames / (float)totalFrames;
state->silenceCount++;
}
}
UnloadWave(wave);
/* Pad speaking portions by shrinking silence regions 0.25s on each side */
if (state->duration > 0.0f)
{
float padNorm = 0.25f / state->duration;
for (int i = 0; i < state->silenceCount; i++)
{
state->silence[i].start += padNorm;
state->silence[i].end -= padNorm;
if (state->silence[i].start >= state->silence[i].end)
{
/* Region too small after padding, remove it */
for (int j = i; j < state->silenceCount - 1; j++)
state->silence[j] = state->silence[j + 1];
state->silenceCount--;
i--;
}
}
}
}
/* ------------------------------------------------------------------ */
/* Portion navigation */
/* ------------------------------------------------------------------ */
int study_find_silence_at(const PlayerState *state, float pos)
{
for (int i = 0; i < state->silenceCount; i++)
if (pos >= state->silence[i].start && pos < state->silence[i].end) return i;
return -1;
}
float study_speaking_portion_start(const PlayerState *state, int portion)
{
if (portion <= 0) return 0.0f;
if (portion > state->silenceCount) return state->silence[state->silenceCount - 1].end;
return state->silence[portion - 1].end;
}
int study_current_speaking_portion(const PlayerState *state, float pos)
{
int portion = 0;
for (int i = 0; i < state->silenceCount; i++)
{
if (pos >= state->silence[i].end)
portion = i + 1;
else
break;
}
return portion;
}
int study_total_speaking_portions(const PlayerState *state)
{
return state->silenceCount + 1;
}
float study_portion_seek_target(const PlayerState *state, int portion)
{
float pos = study_speaking_portion_start(state, portion);
float target = pos * state->duration + (2.0f / 60.0f);
if (target < 0.0f) target = 0.0f;
if (target > state->duration) target = state->duration;
return target;
}
bool study_in_padding_zone(const PlayerState *state, float pos, int portion)
{
if (state->duration <= 0.0f) return false;
float padNorm = 0.25f / state->duration;
float start = study_speaking_portion_start(state, portion);
return (pos >= start && pos < start + padNorm);
}
/* ------------------------------------------------------------------ */
/* Auto-pause logic */
/* ------------------------------------------------------------------ */
void study_auto_pause_check(PlayerState *state, bool smartPlayHeld, bool spaceHeld)
{
if (!state->studyMode || state->duration <= 0.0f || !state->playing)
return;
if (smartPlayHeld || spaceHeld) {
/* Override: still track silence state but don't auto-pause */
float pos = state->currentTime / state->duration;
int silIdx = study_find_silence_at(state, pos);
state->wasInSilence = (silIdx >= 0);
if (silIdx >= 0) state->lastSilenceIdx = silIdx;
return;
}
float pos = state->currentTime / state->duration;
int silIdx = study_find_silence_at(state, pos);
bool nowInSilence = (silIdx >= 0);
if (nowInSilence && !state->wasInSilence)
{
/* Entering silence: seek to start of next speaking portion */
float target = study_portion_seek_target(state, silIdx + 1);
PauseMusicStream(state->music);
SeekMusicStream(state->music, target);
state->currentTime = target;
state->playing = false;
state->skipAutoUpdate = 3;
}
else if (!nowInSilence && state->wasInSilence)
{
/* Exiting silence: land at start of current speaking portion */
int portion = study_current_speaking_portion(state, pos);
float target = study_portion_seek_target(state, portion);
PauseMusicStream(state->music);
SeekMusicStream(state->music, target);
state->currentTime = target;
state->playing = false;
state->skipAutoUpdate = 3;
}
state->wasInSilence = nowInSilence;
if (nowInSilence) state->lastSilenceIdx = silIdx;
}
|