diff options
author | Jaakko Keränen <jaakko.keranen@iki.fi> | 2020-10-04 17:20:41 +0300 |
---|---|---|
committer | Jaakko Keränen <jaakko.keranen@iki.fi> | 2020-10-04 17:20:41 +0300 |
commit | efb40105d657da935d3854e6ea7a513c6210224b (patch) | |
tree | 0327d88d68c0a402f0f90307756bb8536c54dc8b /src | |
parent | 25346114f96a29e8af6125e0cac3d5f8a2ffd551 (diff) |
Working on audio playback
Audio players are displayed the same way as images. When playing, a decoder runs in a background thread producing samples suitable for output.
Diffstat (limited to 'src')
-rw-r--r-- | src/audio/player.c | 337 | ||||
-rw-r--r-- | src/audio/player.h | 5 | ||||
-rw-r--r-- | src/gmdocument.c | 42 | ||||
-rw-r--r-- | src/gmdocument.h | 3 | ||||
-rw-r--r-- | src/gmrequest.c | 3 | ||||
-rw-r--r-- | src/media.c | 135 | ||||
-rw-r--r-- | src/media.h | 24 | ||||
-rw-r--r-- | src/ui/documentwidget.c | 76 |
8 files changed, 524 insertions, 101 deletions
diff --git a/src/audio/player.c b/src/audio/player.c index aea2a998..1dd3c38a 100644 --- a/src/audio/player.c +++ b/src/audio/player.c | |||
@@ -22,12 +22,122 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
22 | 22 | ||
23 | #include "player.h" | 23 | #include "player.h" |
24 | 24 | ||
25 | #include <the_Foundation/buffer.h> | ||
25 | #include <the_Foundation/thread.h> | 26 | #include <the_Foundation/thread.h> |
26 | #include <SDL_audio.h> | 27 | #include <SDL_audio.h> |
27 | 28 | ||
29 | iDeclareType(InputBuf) | ||
30 | |||
31 | struct Impl_InputBuf { | ||
32 | iMutex mtx; | ||
33 | iCondition changed; | ||
34 | iBlock data; | ||
35 | iBool isComplete; | ||
36 | }; | ||
37 | |||
38 | void init_InputBuf(iInputBuf *d) { | ||
39 | init_Mutex(&d->mtx); | ||
40 | init_Condition(&d->changed); | ||
41 | init_Block(&d->data, 0); | ||
42 | d->isComplete = iTrue; | ||
43 | } | ||
44 | |||
45 | void deinit_InputBuf(iInputBuf *d) { | ||
46 | deinit_Block(&d->data); | ||
47 | deinit_Condition(&d->changed); | ||
48 | deinit_Mutex(&d->mtx); | ||
49 | } | ||
50 | |||
51 | size_t size_InputBuf(const iInputBuf *d) { | ||
52 | return size_Block(&d->data); | ||
53 | } | ||
54 | |||
55 | iDefineTypeConstruction(InputBuf) | ||
56 | |||
57 | /*----------------------------------------------------------------------------------------------*/ | ||
58 | |||
59 | iDeclareType(SampleBuf) | ||
60 | |||
61 | struct Impl_SampleBuf { | ||
62 | void * data; | ||
63 | size_t sampleSize; | ||
64 | size_t count; | ||
65 | size_t head, tail; | ||
66 | }; | ||
67 | |||
68 | void init_SampleBuf(iSampleBuf *d, size_t sampleSize, size_t count) { | ||
69 | d->sampleSize = sampleSize; | ||
70 | d->count = count + 1; /* considered empty if head==tail */ | ||
71 | d->data = malloc(d->sampleSize * d->count); | ||
72 | d->head = 0; | ||
73 | d->tail = 0; | ||
74 | } | ||
75 | |||
76 | void deinit_SampleBuf(iSampleBuf *d) { | ||
77 | free(d->data); | ||
78 | } | ||
79 | |||
80 | size_t size_SampleBuf(const iSampleBuf *d) { | ||
81 | return d->head - d->tail; | ||
82 | } | ||
83 | |||
84 | size_t vacancy_SampleBuf(const iSampleBuf *d) { | ||
85 | return d->count - size_SampleBuf(d) - 1; | ||
86 | } | ||
87 | |||
88 | iBool isFull_SampleBuf(const iSampleBuf *d) { | ||
89 | return vacancy_SampleBuf(d) == 0; | ||
90 | } | ||
91 | |||
92 | iLocalDef void *ptr_SampleBuf_(iSampleBuf *d, size_t pos) { | ||
93 | return ((char *) d->data) + (d->sampleSize * pos); | ||
94 | } | ||
95 | |||
96 | void write_SampleBuf(iSampleBuf *d, const void *samples, const size_t n) { | ||
97 | iAssert(n <= vacancy_SampleBuf(d)); | ||
98 | const size_t headPos = d->head % d->count; | ||
99 | const size_t avail = d->count - headPos; | ||
100 | if (n > avail) { | ||
101 | const char *in = samples; | ||
102 | memcpy(ptr_SampleBuf_(d, headPos), in, d->sampleSize * avail); | ||
103 | in += d->sampleSize * avail; | ||
104 | memcpy(ptr_SampleBuf_(d, 0), in, d->sampleSize * (n - avail)); | ||
105 | } | ||
106 | else { | ||
107 | memcpy(ptr_SampleBuf_(d, headPos), samples, d->sampleSize * n); | ||
108 | } | ||
109 | d->head += n; | ||
110 | } | ||
111 | |||
112 | void read_SampleBuf(iSampleBuf *d, const size_t n, void *samples_out) { | ||
113 | iAssert(n <= size_SampleBuf(d)); | ||
114 | const size_t tailPos = d->tail % d->count; | ||
115 | const size_t avail = d->count - tailPos; | ||
116 | if (n > avail) { | ||
117 | char *out = samples_out; | ||
118 | memcpy(out, ptr_SampleBuf_(d, tailPos), d->sampleSize * avail); | ||
119 | out += d->sampleSize * avail; | ||
120 | memcpy(out, ptr_SampleBuf_(d, 0), d->sampleSize * (n - avail)); | ||
121 | } | ||
122 | else { | ||
123 | memcpy(samples_out, ptr_SampleBuf_(d, tailPos), d->sampleSize * n); | ||
124 | } | ||
125 | d->tail += n; | ||
126 | } | ||
127 | |||
128 | /*----------------------------------------------------------------------------------------------*/ | ||
129 | |||
130 | iDeclareType(ContentSpec) | ||
131 | |||
132 | struct Impl_ContentSpec { | ||
133 | SDL_AudioSpec spec; | ||
134 | iRanges wavData; | ||
135 | }; | ||
136 | |||
28 | iDeclareType(Decoder) | 137 | iDeclareType(Decoder) |
29 | 138 | ||
30 | enum iDecoderType { | 139 | enum iDecoderType { |
140 | none_DecoderType, | ||
31 | wav_DecoderType, | 141 | wav_DecoderType, |
32 | mpeg_DecoderType, | 142 | mpeg_DecoderType, |
33 | vorbis_DecoderType, | 143 | vorbis_DecoderType, |
@@ -36,29 +146,189 @@ enum iDecoderType { | |||
36 | 146 | ||
37 | struct Impl_Decoder { | 147 | struct Impl_Decoder { |
38 | enum iDecoderType type; | 148 | enum iDecoderType type; |
39 | size_t inPos; | 149 | iThread * thread; |
40 | // iBlock samples; | 150 | iInputBuf * input; |
151 | size_t inputPos; | ||
152 | iSampleBuf output; | ||
153 | iMutex mtx; /* for output */ | ||
154 | iRanges wavData; | ||
41 | }; | 155 | }; |
42 | 156 | ||
157 | static void parseWav_Decoder_(iDecoder *d, iRanges inputRange) { | ||
158 | lock_Mutex(&d->mtx); | ||
159 | const size_t vacancy = vacancy_SampleBuf(&d->output); | ||
160 | const size_t avail = iMin(inputRange.end - d->inputPos, d->wavData.end - d->inputPos) / | ||
161 | d->output.sampleSize; | ||
162 | const size_t n = iMin(vacancy, avail); | ||
163 | iGuardMutex(&d->input->mtx, /* lock so input array isn't reallocated during the copy */ | ||
164 | write_SampleBuf(&d->output, constData_Block(&d->input->data) + d->inputPos, n)); | ||
165 | d->inputPos += n; | ||
166 | unlock_Mutex(&d->mtx); | ||
167 | } | ||
168 | |||
169 | static iThreadResult run_Decoder_(iThread *thread) { | ||
170 | iDecoder *d = userData_Thread(thread); | ||
171 | while (d->type) { | ||
172 | size_t inputSize = 0; | ||
173 | /* Grab more input. */ { | ||
174 | lock_Mutex(&d->input->mtx); | ||
175 | wait_Condition(&d->input->changed, &d->input->mtx); | ||
176 | inputSize = size_Block(&d->input->data); | ||
177 | unlock_Mutex(&d->input->mtx); | ||
178 | } | ||
179 | iRanges inputRange = { d->inputPos, inputSize }; | ||
180 | iAssert(inputRange.start <= inputRange.end); | ||
181 | if (!d->type) break; | ||
182 | /* Have data to work on and a place to save output? */ | ||
183 | if (!isEmpty_Range(&inputRange) && !isFull_SampleBuf(&d->output)) { | ||
184 | switch (d->type) { | ||
185 | case wav_DecoderType: | ||
186 | parseWav_Decoder_(d, inputRange); | ||
187 | break; | ||
188 | default: | ||
189 | break; | ||
190 | } | ||
191 | } | ||
192 | } | ||
193 | return 0; | ||
194 | } | ||
195 | |||
196 | void init_Decoder(iDecoder *d, iInputBuf *input, const iContentSpec *spec) { | ||
197 | d->type = wav_DecoderType; | ||
198 | d->input = input; | ||
199 | d->inputPos = spec->wavData.start; | ||
200 | init_SampleBuf(&d->output, SDL_AUDIO_BITSIZE(spec->spec.format) / 8 * | ||
201 | spec->spec.channels, spec->spec.samples * 2); | ||
202 | init_Mutex(&d->mtx); | ||
203 | d->thread = new_Thread(run_Decoder_); | ||
204 | setUserData_Thread(d->thread, d); | ||
205 | start_Thread(d->thread); | ||
206 | } | ||
207 | |||
208 | void deinit_Decoder(iDecoder *d) { | ||
209 | d->type = none_DecoderType; | ||
210 | signal_Condition(&d->input->changed); | ||
211 | join_Thread(d->thread); | ||
212 | iRelease(d->thread); | ||
213 | deinit_Mutex(&d->mtx); | ||
214 | deinit_SampleBuf(&d->output); | ||
215 | } | ||
216 | |||
217 | iDefineTypeConstructionArgs(Decoder, (iInputBuf *input, const iContentSpec *spec), | ||
218 | input, spec) | ||
219 | |||
220 | /*----------------------------------------------------------------------------------------------*/ | ||
221 | |||
43 | struct Impl_Player { | 222 | struct Impl_Player { |
44 | SDL_AudioSpec spec; | 223 | SDL_AudioSpec spec; |
45 | SDL_AudioDeviceID device; | 224 | SDL_AudioDeviceID device; |
46 | iMutex mtx; | 225 | iInputBuf *data; |
47 | iBlock data; | 226 | iDecoder *decoder; |
48 | iBool isDataComplete; | ||
49 | }; | 227 | }; |
50 | 228 | ||
229 | iDefineTypeConstruction(Player) | ||
230 | |||
231 | static size_t sampleSize_Player_(const iPlayer *d) { | ||
232 | return d->spec.channels * SDL_AUDIO_BITSIZE(d->spec.format) / 8; | ||
233 | } | ||
234 | |||
235 | static int silence_Player_(const iPlayer *d) { | ||
236 | return d->spec.silence; | ||
237 | } | ||
238 | |||
239 | static iContentSpec contentSpec_Player_(const iPlayer *d) { | ||
240 | iContentSpec content; | ||
241 | iZap(content); | ||
242 | const size_t dataSize = size_InputBuf(d->data); | ||
243 | iBuffer *buf = iClob(new_Buffer()); | ||
244 | open_Buffer(buf, &d->data->data); | ||
245 | enum iDecoderType decType = wav_DecoderType; /* TODO: from MIME */ | ||
246 | if (decType == wav_DecoderType && dataSize >= 44) { | ||
247 | /* Read the RIFF/WAVE header. */ | ||
248 | iStream *is = stream_Buffer(buf); | ||
249 | char magic[4]; | ||
250 | readData_Buffer(buf, 4, magic); | ||
251 | if (memcmp(magic, "RIFF", 4)) { | ||
252 | /* Not WAV. */ | ||
253 | return content; | ||
254 | } | ||
255 | readU32_Stream(is); /* file size */ | ||
256 | readData_Buffer(buf, 4, magic); | ||
257 | if (memcmp(magic, "WAVE", 4)) { | ||
258 | /* Not WAV. */ | ||
259 | return content; | ||
260 | } | ||
261 | /* Read all the chunks. */ | ||
262 | while (!atEnd_Buffer(buf)) { | ||
263 | readData_Buffer(buf, 4, magic); | ||
264 | const size_t size = read32_Stream(is); | ||
265 | if (memcmp(magic, "fmt ", 4) == 0) { | ||
266 | if (size != 16) { | ||
267 | return content; | ||
268 | } | ||
269 | const int16_t mode = read16_Stream(is); /* 1 = PCM */ | ||
270 | const int16_t numChannels = read16_Stream(is); | ||
271 | const int32_t freq = read32_Stream(is); | ||
272 | const uint32_t bytesPerSecond = readU32_Stream(is); | ||
273 | const int16_t blockAlign = read16_Stream(is); | ||
274 | const int16_t bitsPerSample = read16_Stream(is); | ||
275 | iUnused(bytesPerSecond); | ||
276 | iUnused(blockAlign); /* TODO: Should use this one when reading samples? */ | ||
277 | if (mode != 1) { /* PCM */ | ||
278 | return content; | ||
279 | } | ||
280 | if (numChannels != 1 && numChannels != 2) { | ||
281 | return content; | ||
282 | } | ||
283 | if (bitsPerSample != 8 && bitsPerSample != 16 && bitsPerSample != 32) { | ||
284 | return content; | ||
285 | } | ||
286 | content.spec.freq = freq; | ||
287 | content.spec.channels = numChannels; | ||
288 | content.spec.format = | ||
289 | (bitsPerSample == 8 ? AUDIO_S8 : bitsPerSample == 16 ? AUDIO_S16 : AUDIO_S32); | ||
290 | } | ||
291 | else if (memcmp(magic, "data", 4) == 0) { | ||
292 | size_t len = read32_Stream(is); /* data size */ | ||
293 | content.wavData = (iRanges){ pos_Stream(is), pos_Stream(is) + len }; | ||
294 | break; | ||
295 | } | ||
296 | else { | ||
297 | seek_Stream(is, pos_Stream(is) + size); | ||
298 | } | ||
299 | } | ||
300 | } | ||
301 | content.spec.samples = 2048; | ||
302 | return content; | ||
303 | } | ||
304 | |||
305 | static void writeOutputSamples_Player_(void *plr, Uint8 *stream, int len) { | ||
306 | iPlayer *d = plr; | ||
307 | iAssert(d->decoder); | ||
308 | const size_t sampleSize = sampleSize_Player_(d); | ||
309 | const size_t count = len / sampleSize; | ||
310 | lock_Mutex(&d->decoder->mtx); | ||
311 | if (size_SampleBuf(&d->decoder->output) >= count) { | ||
312 | read_SampleBuf(&d->decoder->output, count, stream); | ||
313 | } | ||
314 | else { | ||
315 | memset(stream, 0, len); /* TODO: If unsigned samples, don't use zero! */ | ||
316 | } | ||
317 | unlock_Mutex(&d->decoder->mtx); | ||
318 | /* Wake up decoder; there is more room for output. */ | ||
319 | signal_Condition(&d->data->changed); | ||
320 | } | ||
321 | |||
51 | void init_Player(iPlayer *d) { | 322 | void init_Player(iPlayer *d) { |
52 | iZap(d->spec); | 323 | iZap(d->spec); |
53 | d->device = 0; | 324 | d->device = 0; |
54 | init_Mutex(&d->mtx); | 325 | d->decoder = NULL; |
55 | init_Block(&d->data, 0); | 326 | d->data = new_InputBuf(); |
56 | d->isDataComplete = iFalse; | ||
57 | } | 327 | } |
58 | 328 | ||
59 | void deinit_Player(iPlayer *d) { | 329 | void deinit_Player(iPlayer *d) { |
60 | stop_Player(d); | 330 | stop_Player(d); |
61 | deinit_Block(&d->data); | 331 | delete_InputBuf(d->data); |
62 | } | 332 | } |
63 | 333 | ||
64 | iBool isStarted_Player(const iPlayer *d) { | 334 | iBool isStarted_Player(const iPlayer *d) { |
@@ -70,55 +340,54 @@ void setFormatHint_Player(iPlayer *d, const char *hint) { | |||
70 | } | 340 | } |
71 | 341 | ||
72 | void updateSourceData_Player(iPlayer *d, const iBlock *data, enum iPlayerUpdate update) { | 342 | void updateSourceData_Player(iPlayer *d, const iBlock *data, enum iPlayerUpdate update) { |
73 | lock_Mutex(&d->mtx); | 343 | iInputBuf *inp = d->data; |
344 | lock_Mutex(&inp->mtx); | ||
74 | switch (update) { | 345 | switch (update) { |
75 | case replace_PlayerUpdate: | 346 | case replace_PlayerUpdate: |
76 | set_Block(&d->data, data); | 347 | set_Block(&inp->data, data); |
77 | d->isDataComplete = iFalse; | 348 | inp->isComplete = iFalse; |
78 | break; | 349 | break; |
79 | case append_PlayerUpdate: | 350 | case append_PlayerUpdate: |
80 | append_Block(&d->data, data); | 351 | append_Block(&inp->data, data); |
81 | d->isDataComplete = iFalse; | 352 | inp->isComplete = iFalse; |
82 | break; | 353 | break; |
83 | case complete_PlayerUpdate: | 354 | case complete_PlayerUpdate: |
84 | d->isDataComplete = iTrue; | 355 | inp->isComplete = iTrue; |
85 | break; | 356 | break; |
86 | } | 357 | } |
87 | unlock_Mutex(&d->mtx); | 358 | signal_Condition(&inp->changed); |
88 | } | 359 | unlock_Mutex(&inp->mtx); |
89 | |||
90 | static void writeOutputSamples_Player_(void *plr, Uint8 *stream, int len) { | ||
91 | iPlayer *d = plr; | ||
92 | memset(stream, 0, len); | ||
93 | /* TODO: Copy samples from the decoder's ring buffer. */ | ||
94 | } | 360 | } |
95 | 361 | ||
96 | iBool start_Player(iPlayer *d) { | 362 | iBool start_Player(iPlayer *d) { |
97 | if (isStarted_Player(d)) { | 363 | if (isStarted_Player(d)) { |
98 | return iFalse; | 364 | return iFalse; |
99 | } | 365 | } |
100 | SDL_AudioSpec conf; | 366 | iContentSpec content = contentSpec_Player_(d); |
101 | iZap(conf); | 367 | content.spec.callback = writeOutputSamples_Player_; |
102 | conf.freq = 44100; /* TODO: from content */ | 368 | content.spec.userdata = d; |
103 | conf.format = AUDIO_S16; | 369 | d->device = SDL_OpenAudioDevice(NULL, SDL_FALSE /* playback */, &content.spec, &d->spec, 0); |
104 | conf.channels = 2; /* TODO: from content */ | ||
105 | conf.samples = 2048; | ||
106 | conf.callback = writeOutputSamples_Player_; | ||
107 | conf.userdata = d; | ||
108 | d->device = SDL_OpenAudioDevice(NULL, SDL_FALSE /* playback */, &conf, &d->spec, 0); | ||
109 | if (!d->device) { | 370 | if (!d->device) { |
110 | return iFalse; | 371 | return iFalse; |
111 | } | 372 | } |
112 | /* TODO: Start the stream/decoder thread. */ | 373 | d->decoder = new_Decoder(d->data, &content); |
113 | /* TODO: Audio device is unpaused when there are samples ready to play. */ | 374 | SDL_PauseAudioDevice(d->device, SDL_FALSE); |
114 | return iTrue; | 375 | return iTrue; |
115 | } | 376 | } |
116 | 377 | ||
378 | void setPaused_Player(iPlayer *d, iBool isPaused) { | ||
379 | if (isStarted_Player(d)) { | ||
380 | SDL_PauseAudioDevice(d->device, isPaused ? SDL_TRUE : SDL_FALSE); | ||
381 | } | ||
382 | } | ||
383 | |||
117 | void stop_Player(iPlayer *d) { | 384 | void stop_Player(iPlayer *d) { |
118 | if (isStarted_Player(d)) { | 385 | if (isStarted_Player(d)) { |
119 | /* TODO: Stop the stream/decoder. */ | 386 | /* TODO: Stop the stream/decoder. */ |
120 | SDL_PauseAudioDevice(d->device, SDL_TRUE); | 387 | SDL_PauseAudioDevice(d->device, SDL_TRUE); |
121 | SDL_CloseAudioDevice(d->device); | 388 | SDL_CloseAudioDevice(d->device); |
122 | d->device = 0; | 389 | d->device = 0; |
390 | delete_Decoder(d->decoder); | ||
391 | d->decoder = NULL; | ||
123 | } | 392 | } |
124 | } | 393 | } |
diff --git a/src/audio/player.h b/src/audio/player.h index 121659f2..2138c556 100644 --- a/src/audio/player.h +++ b/src/audio/player.h | |||
@@ -36,7 +36,8 @@ enum iPlayerUpdate { | |||
36 | void setFormatHint_Player (iPlayer *, const char *hint); | 36 | void setFormatHint_Player (iPlayer *, const char *hint); |
37 | void updateSourceData_Player (iPlayer *, const iBlock *data, enum iPlayerUpdate update); | 37 | void updateSourceData_Player (iPlayer *, const iBlock *data, enum iPlayerUpdate update); |
38 | 38 | ||
39 | iBool start_Player (iPlayer *); | 39 | iBool start_Player (iPlayer *); |
40 | void stop_Player (iPlayer *); | 40 | void setPaused_Player (iPlayer *, iBool isPaused); |
41 | void stop_Player (iPlayer *); | ||
41 | 42 | ||
42 | iBool isStarted_Player (const iPlayer *); | 43 | iBool isStarted_Player (const iPlayer *); |
diff --git a/src/gmdocument.c b/src/gmdocument.c index 6e139e45..ff0e019b 100644 --- a/src/gmdocument.c +++ b/src/gmdocument.c | |||
@@ -308,6 +308,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
308 | run.color = white_ColorId; | 308 | run.color = white_ColorId; |
309 | run.linkId = 0; | 309 | run.linkId = 0; |
310 | run.imageId = 0; | 310 | run.imageId = 0; |
311 | run.audioId = 0; | ||
311 | enum iGmLineType type; | 312 | enum iGmLineType type; |
312 | int indent = 0; | 313 | int indent = 0; |
313 | /* Detect the type of the line. */ | 314 | /* Detect the type of the line. */ |
@@ -429,12 +430,12 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
429 | } | 430 | } |
430 | /* Quote icon. */ | 431 | /* Quote icon. */ |
431 | if (type == quote_GmLineType && addQuoteIcon) { | 432 | if (type == quote_GmLineType && addQuoteIcon) { |
432 | addQuoteIcon = iFalse; | 433 | addQuoteIcon = iFalse; |
433 | iGmRun quoteRun = run; | 434 | iGmRun quoteRun = run; |
434 | quoteRun.font = heading1_FontId; | 435 | quoteRun.font = heading1_FontId; |
435 | quoteRun.text = range_CStr(quote); | 436 | quoteRun.text = range_CStr(quote); |
436 | quoteRun.color = tmQuoteIcon_ColorId; | 437 | quoteRun.color = tmQuoteIcon_ColorId; |
437 | iRect vis = visualBounds_Text(quoteRun.font, quoteRun.text); | 438 | iRect vis = visualBounds_Text(quoteRun.font, quoteRun.text); |
438 | quoteRun.visBounds.size = advance_Text(quoteRun.font, quote); | 439 | quoteRun.visBounds.size = advance_Text(quoteRun.font, quote); |
439 | quoteRun.visBounds.pos = | 440 | quoteRun.visBounds.pos = |
440 | add_I2(pos, | 441 | add_I2(pos, |
@@ -522,6 +523,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
522 | /* Image content. */ | 523 | /* Image content. */ |
523 | if (type == link_GmLineType) { | 524 | if (type == link_GmLineType) { |
524 | const iMediaId imageId = findLinkImage_Media(d->media, run.linkId); | 525 | const iMediaId imageId = findLinkImage_Media(d->media, run.linkId); |
526 | const iMediaId audioId = !imageId ? findLinkAudio_Media(d->media, run.linkId) : 0; | ||
525 | if (imageId) { | 527 | if (imageId) { |
526 | iGmImageInfo img; | 528 | iGmImageInfo img; |
527 | imageInfo_Media(d->media, imageId, &img); | 529 | imageInfo_Media(d->media, imageId, &img); |
@@ -532,7 +534,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
532 | link->flags |= permanent_GmLinkFlag; | 534 | link->flags |= permanent_GmLinkFlag; |
533 | } | 535 | } |
534 | } | 536 | } |
535 | const int margin = 0.5f * lineHeight_Text(paragraph_FontId); | 537 | const int margin = lineHeight_Text(paragraph_FontId) / 2; |
536 | pos.y += margin; | 538 | pos.y += margin; |
537 | run.bounds.pos = pos; | 539 | run.bounds.pos = pos; |
538 | run.bounds.size.x = d->size.x; | 540 | run.bounds.size.x = d->size.x; |
@@ -554,6 +556,28 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
554 | pushBack_Array(&d->layout, &run); | 556 | pushBack_Array(&d->layout, &run); |
555 | pos.y += run.bounds.size.y + margin; | 557 | pos.y += run.bounds.size.y + margin; |
556 | } | 558 | } |
559 | else if (audioId) { | ||
560 | iGmAudioInfo info; | ||
561 | audioInfo_Media(d->media, audioId, &info); | ||
562 | /* Mark the link as having content. */ { | ||
563 | iGmLink *link = at_PtrArray(&d->links, run.linkId - 1); | ||
564 | link->flags |= content_GmLinkFlag; | ||
565 | if (info.isPermanent) { | ||
566 | link->flags |= permanent_GmLinkFlag; | ||
567 | } | ||
568 | } | ||
569 | const int margin = lineHeight_Text(paragraph_FontId) / 2; | ||
570 | pos.y += margin; | ||
571 | run.bounds.pos = pos; | ||
572 | run.bounds.size.x = d->size.x; | ||
573 | run.bounds.size.y = 2 * lineHeight_Text(uiContent_FontId); | ||
574 | run.visBounds = run.bounds; | ||
575 | run.text = iNullRange; | ||
576 | run.color = 0; | ||
577 | run.audioId = audioId; | ||
578 | pushBack_Array(&d->layout, &run); | ||
579 | pos.y += run.bounds.size.y + margin; | ||
580 | } | ||
557 | } | 581 | } |
558 | prevType = type; | 582 | prevType = type; |
559 | } | 583 | } |
@@ -1120,10 +1144,14 @@ const iTime *linkTime_GmDocument(const iGmDocument *d, iGmLinkId linkId) { | |||
1120 | return link ? &link->when : NULL; | 1144 | return link ? &link->when : NULL; |
1121 | } | 1145 | } |
1122 | 1146 | ||
1123 | uint16_t linkImage_GmDocument(const iGmDocument *d, iGmLinkId linkId) { | 1147 | iMediaId linkImage_GmDocument(const iGmDocument *d, iGmLinkId linkId) { |
1124 | return findLinkImage_Media(d->media, linkId); | 1148 | return findLinkImage_Media(d->media, linkId); |
1125 | } | 1149 | } |
1126 | 1150 | ||
1151 | iMediaId linkAudio_GmDocument(const iGmDocument *d, iGmLinkId linkId) { | ||
1152 | return findLinkAudio_Media(d->media, linkId); | ||
1153 | } | ||
1154 | |||
1127 | enum iColorId linkColor_GmDocument(const iGmDocument *d, iGmLinkId linkId, enum iGmLinkPart part) { | 1155 | enum iColorId linkColor_GmDocument(const iGmDocument *d, iGmLinkId linkId, enum iGmLinkPart part) { |
1128 | const iGmLink *link = link_GmDocument_(d, linkId); | 1156 | const iGmLink *link = link_GmDocument_(d, linkId); |
1129 | const int www_GmLinkFlag = http_GmLinkFlag | mailto_GmLinkFlag; | 1157 | const int www_GmLinkFlag = http_GmLinkFlag | mailto_GmLinkFlag; |
diff --git a/src/gmdocument.h b/src/gmdocument.h index a04398d9..19a6036f 100644 --- a/src/gmdocument.h +++ b/src/gmdocument.h | |||
@@ -132,7 +132,8 @@ const iGmRun * findRun_GmDocument (const iGmDocument *, iInt2 pos); | |||
132 | const char * findLoc_GmDocument (const iGmDocument *, iInt2 pos); | 132 | const char * findLoc_GmDocument (const iGmDocument *, iInt2 pos); |
133 | const iGmRun * findRunAtLoc_GmDocument (const iGmDocument *, const char *loc); | 133 | const iGmRun * findRunAtLoc_GmDocument (const iGmDocument *, const char *loc); |
134 | const iString * linkUrl_GmDocument (const iGmDocument *, iGmLinkId linkId); | 134 | const iString * linkUrl_GmDocument (const iGmDocument *, iGmLinkId linkId); |
135 | uint16_t linkImage_GmDocument (const iGmDocument *, iGmLinkId linkId); | 135 | iMediaId linkImage_GmDocument (const iGmDocument *, iGmLinkId linkId); |
136 | iMediaId linkAudio_GmDocument (const iGmDocument *, iGmLinkId linkId); | ||
136 | int linkFlags_GmDocument (const iGmDocument *, iGmLinkId linkId); | 137 | int linkFlags_GmDocument (const iGmDocument *, iGmLinkId linkId); |
137 | enum iColorId linkColor_GmDocument (const iGmDocument *, iGmLinkId linkId, enum iGmLinkPart part); | 138 | enum iColorId linkColor_GmDocument (const iGmDocument *, iGmLinkId linkId, enum iGmLinkPart part); |
138 | const iTime * linkTime_GmDocument (const iGmDocument *, iGmLinkId linkId); | 139 | const iTime * linkTime_GmDocument (const iGmDocument *, iGmLinkId linkId); |
diff --git a/src/gmrequest.c b/src/gmrequest.c index b667c82a..161c654c 100644 --- a/src/gmrequest.c +++ b/src/gmrequest.c | |||
@@ -454,6 +454,9 @@ void submit_GmRequest(iGmRequest *d) { | |||
454 | else if (endsWithCase_String(path, ".gif")) { | 454 | else if (endsWithCase_String(path, ".gif")) { |
455 | setCStr_String(&d->resp.meta, "image/gif"); | 455 | setCStr_String(&d->resp.meta, "image/gif"); |
456 | } | 456 | } |
457 | else if (endsWithCase_String(path, ".wav")) { | ||
458 | setCStr_String(&d->resp.meta, "audio/wave"); | ||
459 | } | ||
457 | else { | 460 | else { |
458 | setCStr_String(&d->resp.meta, "application/octet-stream"); | 461 | setCStr_String(&d->resp.meta, "application/octet-stream"); |
459 | } | 462 | } |
diff --git a/src/media.c b/src/media.c index 9f6acd19..5d88fbc4 100644 --- a/src/media.c +++ b/src/media.c | |||
@@ -23,26 +23,44 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
23 | #include "media.h" | 23 | #include "media.h" |
24 | #include "gmdocument.h" | 24 | #include "gmdocument.h" |
25 | #include "ui/window.h" | 25 | #include "ui/window.h" |
26 | #include "audio/player.h" | ||
26 | 27 | ||
27 | #include <the_Foundation/ptrarray.h> | 28 | #include <the_Foundation/ptrarray.h> |
28 | #include <stb_image.h> | 29 | #include <stb_image.h> |
29 | #include <SDL_hints.h> | 30 | #include <SDL_hints.h> |
30 | #include <SDL_render.h> | 31 | #include <SDL_render.h> |
31 | 32 | ||
33 | iDeclareType(GmMediaProps) | ||
34 | |||
35 | struct Impl_GmMediaProps { | ||
36 | iGmLinkId linkId; | ||
37 | iString mime; | ||
38 | iBool isPermanent; | ||
39 | }; | ||
40 | |||
41 | static void init_GmMediaProps_(iGmMediaProps *d) { | ||
42 | d->linkId = 0; | ||
43 | init_String(&d->mime); | ||
44 | d->isPermanent = iFalse; | ||
45 | } | ||
46 | |||
47 | static void deinit_GmMediaProps_(iGmMediaProps *d) { | ||
48 | deinit_String(&d->mime); | ||
49 | } | ||
50 | |||
51 | /*----------------------------------------------------------------------------------------------*/ | ||
52 | |||
32 | iDeclareType(GmImage) | 53 | iDeclareType(GmImage) |
33 | 54 | ||
34 | struct Impl_GmImage { | 55 | struct Impl_GmImage { |
56 | iGmMediaProps props; | ||
35 | iInt2 size; | 57 | iInt2 size; |
36 | size_t numBytes; | 58 | size_t numBytes; |
37 | iString mime; | ||
38 | iGmLinkId linkId; | ||
39 | iBool isPermanent; | ||
40 | SDL_Texture *texture; | 59 | SDL_Texture *texture; |
41 | }; | 60 | }; |
42 | 61 | ||
43 | void init_GmImage(iGmImage *d, const iBlock *data) { | 62 | void init_GmImage(iGmImage *d, const iBlock *data) { |
44 | init_String(&d->mime); | 63 | init_GmMediaProps_(&d->props); |
45 | d->isPermanent = iFalse; | ||
46 | d->numBytes = size_Block(data); | 64 | d->numBytes = size_Block(data); |
47 | uint8_t *imgData = stbi_load_from_memory( | 65 | uint8_t *imgData = stbi_load_from_memory( |
48 | constData_Block(data), size_Block(data), &d->size.x, &d->size.y, NULL, 4); | 66 | constData_Block(data), size_Block(data), &d->size.x, &d->size.y, NULL, 4); |
@@ -60,30 +78,53 @@ void init_GmImage(iGmImage *d, const iBlock *data) { | |||
60 | SDL_FreeSurface(surface); | 78 | SDL_FreeSurface(surface); |
61 | stbi_image_free(imgData); | 79 | stbi_image_free(imgData); |
62 | } | 80 | } |
63 | d->linkId = 0; | ||
64 | } | 81 | } |
65 | 82 | ||
66 | void deinit_GmImage(iGmImage *d) { | 83 | void deinit_GmImage(iGmImage *d) { |
67 | SDL_DestroyTexture(d->texture); | 84 | SDL_DestroyTexture(d->texture); |
68 | deinit_String(&d->mime); | 85 | deinit_GmMediaProps_(&d->props); |
69 | } | 86 | } |
70 | 87 | ||
71 | iDefineTypeConstructionArgs(GmImage, (const iBlock *data), data) | 88 | iDefineTypeConstructionArgs(GmImage, (const iBlock *data), data) |
72 | 89 | ||
73 | /*----------------------------------------------------------------------------------------------*/ | 90 | /*----------------------------------------------------------------------------------------------*/ |
74 | 91 | ||
92 | iDeclareType(GmAudio) | ||
93 | |||
94 | struct Impl_GmAudio { | ||
95 | iGmMediaProps props; | ||
96 | iPlayer *player; | ||
97 | }; | ||
98 | |||
99 | void init_GmAudio(iGmAudio *d) { | ||
100 | init_GmMediaProps_(&d->props); | ||
101 | d->player = new_Player(); | ||
102 | } | ||
103 | |||
104 | void deinit_GmAudio(iGmAudio *d) { | ||
105 | delete_Player(d->player); | ||
106 | deinit_GmMediaProps_(&d->props); | ||
107 | } | ||
108 | |||
109 | iDefineTypeConstruction(GmAudio) | ||
110 | |||
111 | /*----------------------------------------------------------------------------------------------*/ | ||
112 | |||
75 | struct Impl_Media { | 113 | struct Impl_Media { |
76 | iPtrArray images; | 114 | iPtrArray images; |
115 | iPtrArray audio; | ||
77 | }; | 116 | }; |
78 | 117 | ||
79 | iDefineTypeConstruction(Media) | 118 | iDefineTypeConstruction(Media) |
80 | 119 | ||
81 | void init_Media(iMedia *d) { | 120 | void init_Media(iMedia *d) { |
82 | init_PtrArray(&d->images); | 121 | init_PtrArray(&d->images); |
122 | init_PtrArray(&d->audio); | ||
83 | } | 123 | } |
84 | 124 | ||
85 | void deinit_Media(iMedia *d) { | 125 | void deinit_Media(iMedia *d) { |
86 | clear_Media(d); | 126 | clear_Media(d); |
127 | deinit_PtrArray(&d->audio); | ||
87 | deinit_PtrArray(&d->images); | 128 | deinit_PtrArray(&d->images); |
88 | } | 129 | } |
89 | 130 | ||
@@ -92,25 +133,35 @@ void clear_Media(iMedia *d) { | |||
92 | deinit_GmImage(i.ptr); | 133 | deinit_GmImage(i.ptr); |
93 | } | 134 | } |
94 | clear_PtrArray(&d->images); | 135 | clear_PtrArray(&d->images); |
136 | iForEach(PtrArray, a, &d->audio) { | ||
137 | deinit_GmAudio(a.ptr); | ||
138 | } | ||
139 | clear_PtrArray(&d->audio); | ||
95 | } | 140 | } |
96 | 141 | ||
97 | void setImage_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlock *data, | 142 | void setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlock *data, |
98 | iBool allowHide) { | 143 | iBool allowHide) { |
99 | if (!mime || !data) { | 144 | if (!mime || !data) { |
100 | iGmImage *img; | 145 | iMediaId existing = findLinkImage_Media(d, linkId); |
101 | const iMediaId existing = findLinkImage_Media(d, linkId); | ||
102 | if (existing) { | 146 | if (existing) { |
147 | iGmImage *img; | ||
103 | take_PtrArray(&d->images, existing - 1, (void **) &img); | 148 | take_PtrArray(&d->images, existing - 1, (void **) &img); |
104 | delete_GmImage(img); | 149 | delete_GmImage(img); |
105 | } | 150 | } |
151 | else if ((existing = findLinkAudio_Media(d, linkId)) != 0) { | ||
152 | iGmAudio *audio; | ||
153 | take_PtrArray(&d->audio, existing - 1, (void **) &audio); | ||
154 | delete_GmAudio(audio); | ||
155 | } | ||
106 | } | 156 | } |
107 | else { | 157 | else { |
108 | /* TODO: check if we know this MIME type */ | 158 | if (startsWith_String(mime, "image/")) { |
109 | /* Upload the image. */ { | 159 | /* Copy the image to a texture. */ |
160 | /* TODO: Resize down to min(maximum texture size, window size). */ | ||
110 | iGmImage *img = new_GmImage(data); | 161 | iGmImage *img = new_GmImage(data); |
111 | img->linkId = linkId; /* TODO: use a hash? */ | 162 | img->props.linkId = linkId; /* TODO: use a hash? */ |
112 | img->isPermanent = !allowHide; | 163 | img->props.isPermanent = !allowHide; |
113 | set_String(&img->mime, mime); | 164 | set_String(&img->props.mime, mime); |
114 | if (img->texture) { | 165 | if (img->texture) { |
115 | pushBack_PtrArray(&d->images, img); | 166 | pushBack_PtrArray(&d->images, img); |
116 | } | 167 | } |
@@ -118,15 +169,34 @@ void setImage_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo | |||
118 | delete_GmImage(img); | 169 | delete_GmImage(img); |
119 | } | 170 | } |
120 | } | 171 | } |
172 | else if (startsWith_String(mime, "audio/")) { | ||
173 | iGmAudio *audio = new_GmAudio(); | ||
174 | audio->props.linkId = linkId; | ||
175 | audio->props.isPermanent = !allowHide; | ||
176 | set_String(&audio->props.mime, mime); | ||
177 | /* TODO: What about update/complete, for streaming? */ | ||
178 | updateSourceData_Player(audio->player, data, replace_PlayerUpdate); | ||
179 | pushBack_PtrArray(&d->audio, audio); | ||
180 | } | ||
121 | } | 181 | } |
122 | // doLayout_GmDocument_(d); | ||
123 | } | 182 | } |
124 | 183 | ||
125 | iMediaId findLinkImage_Media(const iMedia *d, iGmLinkId linkId) { | 184 | iMediaId findLinkImage_Media(const iMedia *d, iGmLinkId linkId) { |
126 | /* TODO: use a hash */ | 185 | /* TODO: use a hash */ |
127 | iConstForEach(PtrArray, i, &d->images) { | 186 | iConstForEach(PtrArray, i, &d->images) { |
128 | const iGmImage *img = i.ptr; | 187 | const iGmImage *img = i.ptr; |
129 | if (img->linkId == linkId) { | 188 | if (img->props.linkId == linkId) { |
189 | return index_PtrArrayConstIterator(&i) + 1; | ||
190 | } | ||
191 | } | ||
192 | return 0; | ||
193 | } | ||
194 | |||
195 | iMediaId findLinkAudio_Media(const iMedia *d, iGmLinkId linkId) { | ||
196 | /* TODO: use a hash */ | ||
197 | iConstForEach(PtrArray, i, &d->audio) { | ||
198 | const iGmAudio *audio = i.ptr; | ||
199 | if (audio->props.linkId == linkId) { | ||
130 | return index_PtrArrayConstIterator(&i) + 1; | 200 | return index_PtrArrayConstIterator(&i) + 1; |
131 | } | 201 | } |
132 | } | 202 | } |
@@ -141,15 +211,34 @@ SDL_Texture *imageTexture_Media(const iMedia *d, uint16_t imageId) { | |||
141 | return NULL; | 211 | return NULL; |
142 | } | 212 | } |
143 | 213 | ||
144 | void imageInfo_Media(const iMedia *d, uint16_t imageId, iGmImageInfo *info_out) { | 214 | iBool imageInfo_Media(const iMedia *d, iMediaId imageId, iGmImageInfo *info_out) { |
145 | if (imageId > 0 && imageId <= size_PtrArray(&d->images)) { | 215 | if (imageId > 0 && imageId <= size_PtrArray(&d->images)) { |
146 | const iGmImage *img = constAt_PtrArray(&d->images, imageId - 1); | 216 | const iGmImage *img = constAt_PtrArray(&d->images, imageId - 1); |
147 | info_out->size = img->size; | 217 | info_out->size = img->size; |
148 | info_out->numBytes = img->numBytes; | 218 | info_out->numBytes = img->numBytes; |
149 | info_out->mime = cstr_String(&img->mime); | 219 | info_out->mime = cstr_String(&img->props.mime); |
150 | info_out->isPermanent = img->isPermanent; | 220 | info_out->isPermanent = img->props.isPermanent; |
221 | return iTrue; | ||
151 | } | 222 | } |
152 | else { | 223 | iZap(*info_out); |
153 | iZap(*info_out); | 224 | return iFalse; |
225 | } | ||
226 | |||
227 | iPlayer *audioData_Media(const iMedia *d, iMediaId audioId) { | ||
228 | if (audioId > 0 && audioId <= size_PtrArray(&d->audio)) { | ||
229 | const iGmAudio *audio = constAt_PtrArray(&d->audio, audioId - 1); | ||
230 | return audio->player; | ||
231 | } | ||
232 | return NULL; | ||
233 | } | ||
234 | |||
235 | iBool audioInfo_Media(const iMedia *d, iMediaId audioId, iGmAudioInfo *info_out) { | ||
236 | if (audioId > 0 && audioId <= size_PtrArray(&d->audio)) { | ||
237 | const iGmAudio *audio = constAt_PtrArray(&d->audio, audioId - 1); | ||
238 | info_out->mime = cstr_String(&audio->props.mime); | ||
239 | info_out->isPermanent = audio->props.isPermanent; | ||
240 | return iTrue; | ||
154 | } | 241 | } |
242 | iZap(*info_out); | ||
243 | return iFalse; | ||
155 | } | 244 | } |
diff --git a/src/media.h b/src/media.h index 0a6c1a81..6f460422 100644 --- a/src/media.h +++ b/src/media.h | |||
@@ -29,20 +29,32 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
29 | 29 | ||
30 | typedef uint16_t iMediaId; | 30 | typedef uint16_t iMediaId; |
31 | 31 | ||
32 | iDeclareType(Player) | ||
32 | iDeclareType(GmImageInfo) | 33 | iDeclareType(GmImageInfo) |
34 | iDeclareType(GmAudioInfo) | ||
33 | 35 | ||
34 | struct Impl_GmImageInfo { | 36 | struct Impl_GmImageInfo { |
35 | iInt2 size; | 37 | iInt2 size; |
36 | size_t numBytes; | 38 | size_t numBytes; |
37 | const char *mime; | 39 | const char *mime; |
38 | iBool isPermanent; | 40 | iBool isPermanent; |
39 | }; | 41 | }; |
40 | 42 | ||
41 | iDeclareType(Media) iDeclareTypeConstruction(Media) | 43 | struct Impl_GmAudioInfo { |
44 | const char *mime; | ||
45 | iBool isPermanent; | ||
46 | }; | ||
47 | |||
48 | iDeclareType(Media) | ||
49 | iDeclareTypeConstruction(Media) | ||
42 | 50 | ||
43 | void clear_Media (iMedia *); | 51 | void clear_Media (iMedia *); |
44 | void setImage_Media (iMedia *, uint16_t linkId, const iString *mime, const iBlock *data, iBool allowHide); | 52 | void setData_Media (iMedia *, uint16_t linkId, const iString *mime, const iBlock *data, iBool allowHide); |
45 | 53 | ||
46 | iMediaId findLinkImage_Media (const iMedia *, uint16_t linkId); | 54 | iMediaId findLinkImage_Media (const iMedia *, uint16_t linkId); |
55 | iBool imageInfo_Media (const iMedia *, iMediaId imageId, iGmImageInfo *info_out); | ||
47 | SDL_Texture * imageTexture_Media (const iMedia *, iMediaId imageId); | 56 | SDL_Texture * imageTexture_Media (const iMedia *, iMediaId imageId); |
48 | void imageInfo_Media (const iMedia *, iMediaId imageId, iGmImageInfo *info_out); | 57 | |
58 | iMediaId findLinkAudio_Media (const iMedia *, uint16_t linkId); | ||
59 | iBool audioInfo_Media (const iMedia *, iMediaId audioId, iGmAudioInfo *info_out); | ||
60 | iPlayer * audioPlayer_Media (const iMedia *, iMediaId audioId); | ||
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index f63d93f4..be9da513 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c | |||
@@ -723,24 +723,26 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse | |||
723 | docFormat = gemini_GmDocumentFormat; | 723 | docFormat = gemini_GmDocumentFormat; |
724 | setRange_String(&d->sourceMime, param); | 724 | setRange_String(&d->sourceMime, param); |
725 | } | 725 | } |
726 | else if (startsWith_Rangecc(param, "image/")) { | 726 | else if (startsWith_Rangecc(param, "image/") || |
727 | startsWith_Rangecc(param, "audio/")) { | ||
727 | docFormat = gemini_GmDocumentFormat; | 728 | docFormat = gemini_GmDocumentFormat; |
728 | setRange_String(&d->sourceMime, param); | 729 | setRange_String(&d->sourceMime, param); |
729 | if (!d->request || isFinished_GmRequest(d->request)) { | 730 | if (!d->request || isFinished_GmRequest(d->request)) { |
730 | /* Make a simple document with an image. */ | 731 | /* Make a simple document with an image or audio player. */ |
731 | const char *imageTitle = "Image"; | 732 | const char *linkTitle = |
733 | startsWith_String(mimeStr, "image/") ? "Image" : "Audio"; | ||
732 | iUrl parts; | 734 | iUrl parts; |
733 | init_Url(&parts, d->mod.url); | 735 | init_Url(&parts, d->mod.url); |
734 | if (!isEmpty_Range(&parts.path)) { | 736 | if (!isEmpty_Range(&parts.path)) { |
735 | imageTitle = | 737 | linkTitle = |
736 | baseName_Path(collect_String(newRange_String(parts.path))).start; | 738 | baseName_Path(collect_String(newRange_String(parts.path))).start; |
737 | } | 739 | } |
738 | format_String(&str, "=> %s %s\n", cstr_String(d->mod.url), imageTitle); | 740 | format_String(&str, "=> %s %s\n", cstr_String(d->mod.url), linkTitle); |
739 | setImage_Media(media_GmDocument(d->doc), | 741 | setData_Media(media_GmDocument(d->doc), |
740 | 1, | 742 | 1, |
741 | mimeStr, | 743 | mimeStr, |
742 | &response->body, | 744 | &response->body, |
743 | iFalse /* it's fixed */); | 745 | iFalse /* it's fixed */); |
744 | redoLayout_GmDocument(d->doc); | 746 | redoLayout_GmDocument(d->doc); |
745 | } | 747 | } |
746 | else { | 748 | else { |
@@ -1132,6 +1134,8 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char * | |||
1132 | return iFalse; /* not our request */ | 1134 | return iFalse; /* not our request */ |
1133 | } | 1135 | } |
1134 | if (equal_Command(cmd, "media.updated")) { | 1136 | if (equal_Command(cmd, "media.updated")) { |
1137 | /* Pass new data to media players. */ | ||
1138 | |||
1135 | /* Update the link's progress. */ | 1139 | /* Update the link's progress. */ |
1136 | invalidateLink_DocumentWidget_(d, req->linkId); | 1140 | invalidateLink_DocumentWidget_(d, req->linkId); |
1137 | refresh_Widget(d); | 1141 | refresh_Widget(d); |
@@ -1141,12 +1145,13 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char * | |||
1141 | const enum iGmStatusCode code = status_GmRequest(req->req); | 1145 | const enum iGmStatusCode code = status_GmRequest(req->req); |
1142 | /* Give the media to the document for presentation. */ | 1146 | /* Give the media to the document for presentation. */ |
1143 | if (code == success_GmStatusCode) { | 1147 | if (code == success_GmStatusCode) { |
1144 | if (startsWith_String(meta_GmRequest(req->req), "image/")) { | 1148 | if (startsWith_String(meta_GmRequest(req->req), "image/") || |
1145 | setImage_Media(media_GmDocument(d->doc), | 1149 | startsWith_String(meta_GmRequest(req->req), "audio/")) { |
1146 | req->linkId, | 1150 | setData_Media(media_GmDocument(d->doc), |
1147 | meta_GmRequest(req->req), | 1151 | req->linkId, |
1148 | body_GmRequest(req->req), | 1152 | meta_GmRequest(req->req), |
1149 | iTrue); | 1153 | body_GmRequest(req->req), |
1154 | iTrue); | ||
1150 | redoLayout_GmDocument(d->doc); | 1155 | redoLayout_GmDocument(d->doc); |
1151 | updateVisible_DocumentWidget_(d); | 1156 | updateVisible_DocumentWidget_(d); |
1152 | invalidate_DocumentWidget_(d); | 1157 | invalidate_DocumentWidget_(d); |
@@ -1793,14 +1798,14 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
1793 | if (isMediaLink_GmDocument(d->doc, linkId)) { | 1798 | if (isMediaLink_GmDocument(d->doc, linkId)) { |
1794 | const int linkFlags = linkFlags_GmDocument(d->doc, linkId); | 1799 | const int linkFlags = linkFlags_GmDocument(d->doc, linkId); |
1795 | if (linkFlags & content_GmLinkFlag && linkFlags & permanent_GmLinkFlag) { | 1800 | if (linkFlags & content_GmLinkFlag && linkFlags & permanent_GmLinkFlag) { |
1796 | /* We have the image and it cannot be dismissed, so nothing | 1801 | /* We have the content and it cannot be dismissed, so nothing |
1797 | further to do. */ | 1802 | further to do. */ |
1798 | return iTrue; | 1803 | return iTrue; |
1799 | } | 1804 | } |
1800 | if (!requestMedia_DocumentWidget_(d, linkId)) { | 1805 | if (!requestMedia_DocumentWidget_(d, linkId)) { |
1801 | if (linkFlags & content_GmLinkFlag) { | 1806 | if (linkFlags & content_GmLinkFlag) { |
1802 | /* Dismiss shown content on click. */ | 1807 | /* Dismiss shown content on click. */ |
1803 | setImage_Media(media_GmDocument(d->doc), linkId, NULL, NULL, iTrue); | 1808 | setData_Media(media_GmDocument(d->doc), linkId, NULL, NULL, iTrue); |
1804 | redoLayout_GmDocument(d->doc); | 1809 | redoLayout_GmDocument(d->doc); |
1805 | d->hoverLink = NULL; | 1810 | d->hoverLink = NULL; |
1806 | scroll_DocumentWidget_(d, 0); | 1811 | scroll_DocumentWidget_(d, 0); |
@@ -1813,11 +1818,11 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
1813 | /* Show the existing content again if we have it. */ | 1818 | /* Show the existing content again if we have it. */ |
1814 | iMediaRequest *req = findMediaRequest_DocumentWidget_(d, linkId); | 1819 | iMediaRequest *req = findMediaRequest_DocumentWidget_(d, linkId); |
1815 | if (req) { | 1820 | if (req) { |
1816 | setImage_Media(media_GmDocument(d->doc), | 1821 | setData_Media(media_GmDocument(d->doc), |
1817 | linkId, | 1822 | linkId, |
1818 | meta_GmRequest(req->req), | 1823 | meta_GmRequest(req->req), |
1819 | body_GmRequest(req->req), | 1824 | body_GmRequest(req->req), |
1820 | iTrue); | 1825 | iTrue); |
1821 | redoLayout_GmDocument(d->doc); | 1826 | redoLayout_GmDocument(d->doc); |
1822 | updateVisible_DocumentWidget_(d); | 1827 | updateVisible_DocumentWidget_(d); |
1823 | invalidate_DocumentWidget_(d); | 1828 | invalidate_DocumentWidget_(d); |
@@ -1913,6 +1918,11 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { | |||
1913 | } | 1918 | } |
1914 | return; | 1919 | return; |
1915 | } | 1920 | } |
1921 | else if (run->audioId) { | ||
1922 | /* Draw the audio player interface. */ | ||
1923 | fillRect_Paint(&d->paint, moved_Rect(run->visBounds, origin), red_ColorId); | ||
1924 | return; | ||
1925 | } | ||
1916 | enum iColorId fg = run->color; | 1926 | enum iColorId fg = run->color; |
1917 | const iGmDocument *doc = d->widget->doc; | 1927 | const iGmDocument *doc = d->widget->doc; |
1918 | const iBool isHover = | 1928 | const iBool isHover = |
@@ -1989,13 +1999,23 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { | |||
1989 | /* Show metadata about inline content. */ | 1999 | /* Show metadata about inline content. */ |
1990 | if (flags & content_GmLinkFlag && run->flags & endOfLine_GmRunFlag) { | 2000 | if (flags & content_GmLinkFlag && run->flags & endOfLine_GmRunFlag) { |
1991 | fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); | 2001 | fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); |
1992 | iAssert(!isEmpty_Rect(run->bounds)); | ||
1993 | iGmImageInfo info; | ||
1994 | imageInfo_Media(constMedia_GmDocument(doc), linkImage_GmDocument(doc, run->linkId), &info); | ||
1995 | iString text; | 2002 | iString text; |
1996 | init_String(&text); | 2003 | init_String(&text); |
1997 | format_String(&text, "%s \u2014 %d x %d \u2014 %.1fMB", | 2004 | iMediaId imageId = linkImage_GmDocument(doc, run->linkId); |
1998 | info.mime, info.size.x, info.size.y, info.numBytes / 1.0e6f); | 2005 | iMediaId audioId = !imageId ? linkAudio_GmDocument(doc, run->linkId) : 0; |
2006 | iAssert(imageId || audioId); | ||
2007 | if (imageId) { | ||
2008 | iAssert(!isEmpty_Rect(run->bounds)); | ||
2009 | iGmImageInfo info; | ||
2010 | imageInfo_Media(constMedia_GmDocument(doc), imageId, &info); | ||
2011 | format_String(&text, "%s \u2014 %d x %d \u2014 %.1fMB", | ||
2012 | info.mime, info.size.x, info.size.y, info.numBytes / 1.0e6f); | ||
2013 | } | ||
2014 | else if (audioId) { | ||
2015 | iGmAudioInfo info; | ||
2016 | audioInfo_Media(constMedia_GmDocument(doc), audioId, &info); | ||
2017 | format_String(&text, "%s", info.mime); | ||
2018 | } | ||
1999 | if (findMediaRequest_DocumentWidget_(d->widget, run->linkId)) { | 2019 | if (findMediaRequest_DocumentWidget_(d->widget, run->linkId)) { |
2000 | appendFormat_String( | 2020 | appendFormat_String( |
2001 | &text, " %s\u2a2f", isHover ? escape_Color(tmLinkText_ColorId) : ""); | 2021 | &text, " %s\u2a2f", isHover ? escape_Color(tmLinkText_ColorId) : ""); |