From 1ffd161fa550d5334df7ee1aaa16bc369a1324b4 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 7 Oct 2020 12:31:31 +0300 Subject: Drawing an audio player UI --- src/audio/player.c | 29 ++++++++- src/audio/player.h | 3 + src/gmdocument.c | 2 +- src/media.c | 8 +++ src/ui/documentwidget.c | 164 +++++++++++++++++++++++++++++++++++++++++++----- 5 files changed, 188 insertions(+), 18 deletions(-) diff --git a/src/audio/player.c b/src/audio/player.c index 177613bb..2d6767ea 100644 --- a/src/audio/player.c +++ b/src/audio/player.c @@ -146,6 +146,7 @@ iDeclareType(ContentSpec) struct Impl_ContentSpec { SDL_AudioFormat inputFormat; SDL_AudioSpec output; + uint64_t totalSamples; iRanges wavData; }; @@ -168,6 +169,8 @@ struct Impl_Decoder { size_t inputPos; iSampleBuf output; iMutex outputMutex; + uint64_t currentSample; + uint64_t totalSamples; /* zero if unknown */ iRanges wavData; }; @@ -252,6 +255,7 @@ static enum iDecoderParseStatus parseWav_Decoder_(iDecoder *d, iRanges inputRang } } iGuardMutex(&d->outputMutex, write_SampleBuf(&d->output, samples, n)); + d->currentSample += n; free(samples); return ok_DecoderParseStatus; } @@ -305,6 +309,8 @@ void init_Decoder(iDecoder *d, iInputBuf *input, const iContentSpec *spec) { spec->output.format, spec->output.channels, spec->output.samples * 2); + d->currentSample = 0; + d->totalSamples = spec->totalSamples; init_Mutex(&d->outputMutex); d->thread = new_Thread(run_Decoder_); setUserData_Thread(d->thread, d); @@ -366,6 +372,7 @@ static iContentSpec contentSpec_Player_(const iPlayer *d) { return content; } /* Read all the chunks. */ + int16_t blockAlign = 0; while (!atEnd_Buffer(buf)) { readData_Buffer(buf, 4, magic); const size_t size = read32_Stream(is); @@ -381,7 +388,7 @@ static iContentSpec contentSpec_Player_(const iPlayer *d) { const int16_t numChannels = read16_Stream(is); const int32_t freq = read32_Stream(is); const uint32_t bytesPerSecond = readU32_Stream(is); - const int16_t blockAlign = read16_Stream(is); + blockAlign = read16_Stream(is); const int16_t bitsPerSample = read16_Stream(is); const uint16_t extSize = (size == 18 ? readU16_Stream(is) : 0); iUnused(bytesPerSecond); @@ -418,7 +425,8 @@ static iContentSpec contentSpec_Player_(const iPlayer *d) { } } else if (memcmp(magic, "data", 4) == 0) { - content.wavData = (iRanges){ pos_Stream(is), pos_Stream(is) + size }; + content.wavData = (iRanges){ pos_Stream(is), pos_Stream(is) + size }; + content.totalSamples = (uint64_t) size_Range(&content.wavData) / blockAlign; break; } else { @@ -465,11 +473,16 @@ iBool isStarted_Player(const iPlayer *d) { return d->device != 0; } -void setFormatHint_Player(iPlayer *d, const char *hint) { +iBool isPaused_Player(const iPlayer *d) { + if (!d->device) return iTrue; + return SDL_GetAudioDeviceStatus(d->device) == SDL_AUDIO_PAUSED; +} +void setFormatHint_Player(iPlayer *d, const char *hint) { } void updateSourceData_Player(iPlayer *d, const iBlock *data, enum iPlayerUpdate update) { + /* TODO: Add MIME as argument */ iInputBuf *input = d->data; lock_Mutex(&input->mtx); switch (update) { @@ -527,3 +540,13 @@ void stop_Player(iPlayer *d) { d->decoder = NULL; } } + +float time_Player(const iPlayer *d) { + if (!d->decoder) return 0; + return (float) ((double) d->decoder->currentSample / (double) d->spec.freq); +} + +float duration_Player(const iPlayer *d) { + if (!d->decoder) return 0; + return (float) ((double) d->decoder->totalSamples / (double) d->spec.freq); +} diff --git a/src/audio/player.h b/src/audio/player.h index 2138c556..5c17ef6c 100644 --- a/src/audio/player.h +++ b/src/audio/player.h @@ -41,3 +41,6 @@ void setPaused_Player (iPlayer *, iBool isPaused); void stop_Player (iPlayer *); iBool isStarted_Player (const iPlayer *); +iBool isPaused_Player (const iPlayer *); +float time_Player (const iPlayer *); +float duration_Player (const iPlayer *); diff --git a/src/gmdocument.c b/src/gmdocument.c index ff0e019b..e2696085 100644 --- a/src/gmdocument.c +++ b/src/gmdocument.c @@ -570,7 +570,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { pos.y += margin; run.bounds.pos = pos; run.bounds.size.x = d->size.x; - run.bounds.size.y = 2 * lineHeight_Text(uiContent_FontId); + run.bounds.size.y = lineHeight_Text(uiContent_FontId) + 3 * gap_UI; run.visBounds = run.bounds; run.text = iNullRange; run.color = 0; diff --git a/src/media.c b/src/media.c index b72ec32c..ddf5d45f 100644 --- a/src/media.c +++ b/src/media.c @@ -266,3 +266,11 @@ iBool audioInfo_Media(const iMedia *d, iMediaId audioId, iGmAudioInfo *info_out) iZap(*info_out); return iFalse; } + +iPlayer *audioPlayer_Media(const iMedia *d, iMediaId audioId) { + if (audioId > 0 && audioId <= size_PtrArray(&d->audio)) { + const iGmAudio *audio = constAt_PtrArray(&d->audio, audioId - 1); + return audio->player; + } + return NULL; +} diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 165bf9cf..27a26d99 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -21,20 +21,22 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "documentwidget.h" -#include "scrollwidget.h" -#include "inputwidget.h" -#include "labelwidget.h" -#include "visbuf.h" -#include "paint.h" -#include "command.h" -#include "keys.h" -#include "util.h" -#include "history.h" + #include "app.h" +#include "audio/player.h" +#include "command.h" #include "gmdocument.h" #include "gmrequest.h" #include "gmutil.h" +#include "history.h" +#include "inputwidget.h" +#include "keys.h" +#include "labelwidget.h" #include "media.h" +#include "paint.h" +#include "scrollwidget.h" +#include "util.h" +#include "visbuf.h" #include #include @@ -170,6 +172,7 @@ struct Impl_DocumentWidget { iRangecc foundMark; int pageMargin; iPtrArray visibleLinks; + iPtrArray visiblePlayers; /* currently playing audio */ const iGmRun * hoverLink; const iGmRun * contextLink; iBool noHoverWhileScrolling; @@ -233,6 +236,7 @@ void init_DocumentWidget(iDocumentWidget *d) { init_String(&d->sourceMime); init_Block(&d->sourceContent, 0); init_PtrArray(&d->visibleLinks); + init_PtrArray(&d->visiblePlayers); init_Click(&d->click, d, SDL_BUTTON_LEFT); addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); d->menu = NULL; /* created when clicking */ @@ -253,6 +257,7 @@ void deinit_DocumentWidget(iDocumentWidget *d) { deinit_Block(&d->sourceContent); deinit_String(&d->sourceMime); iRelease(d->doc); + deinit_PtrArray(&d->visiblePlayers); deinit_PtrArray(&d->visibleLinks); delete_String(d->certSubject); delete_String(d->titleUser); @@ -333,7 +338,7 @@ static iRangei visibleRange_DocumentWidget_(const iDocumentWidget *d) { d->scrollY + height_Rect(bounds_Widget(constAs_Widget(d))) - margin }; } -static void addVisibleLink_DocumentWidget_(void *context, const iGmRun *run) { +static void addVisible_DocumentWidget_(void *context, const iGmRun *run) { iDocumentWidget *d = context; if (~run->flags & decoration_GmRunFlag && !run->imageId) { if (!d->firstVisibleRun) { @@ -341,6 +346,9 @@ static void addVisibleLink_DocumentWidget_(void *context, const iGmRun *run) { } d->lastVisibleRun = run; } + if (run->audioId) { + pushBack_PtrArray(&d->visiblePlayers, run); + } if (run->linkId && linkFlags_GmDocument(d->doc, run->linkId) & supportedProtocol_GmLinkFlag) { pushBack_PtrArray(&d->visibleLinks, run); } @@ -434,6 +442,18 @@ static void updateOutlineOpacity_DocumentWidget_(iDocumentWidget *d) { animate_DocumentWidget_(d); } +static void animatePlayingAudio_DocumentWidget_(void *widget) { + iDocumentWidget *d = widget; + iConstForEach(PtrArray, i, &d->visiblePlayers) { + const iGmRun *run = i.ptr; + iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->audioId); + if (isStarted_Player(plr) && !isPaused_Player(plr)) { + refresh_Widget(d); + addTicker_App(animatePlayingAudio_DocumentWidget_, d); + } + } +} + static void updateVisible_DocumentWidget_(iDocumentWidget *d) { const iRangei visRange = visibleRange_DocumentWidget_(d); const iRect bounds = bounds_Widget(as_Widget(d)); @@ -443,10 +463,12 @@ static void updateVisible_DocumentWidget_(iDocumentWidget *d) { d->scrollY, docSize > 0 ? height_Rect(bounds) * size_Range(&visRange) / docSize : 0); clear_PtrArray(&d->visibleLinks); + clear_PtrArray(&d->visiblePlayers); d->firstVisibleRun = NULL; - render_GmDocument(d->doc, visRange, addVisibleLink_DocumentWidget_, d); + render_GmDocument(d->doc, visRange, addVisible_DocumentWidget_, d); updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window())); updateSideOpacity_DocumentWidget_(d, iTrue); + animatePlayingAudio_DocumentWidget_(d); /* Remember scroll positions of recently visited pages. */ { iRecentUrl *recent = mostRecentUrl_History(d->mod.history); if (recent && docSize && d->state == ready_RequestState) { @@ -1919,8 +1941,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { return; } else if (run->audioId) { - /* Draw the audio player interface. */ - fillRect_Paint(&d->paint, moved_Rect(run->visBounds, origin), red_ColorId); + /* Audio player UI is drawn afterwards as a dynamic overlay. */ + //fillRect_Paint(&d->paint, moved_Rect(run->visBounds, origin), red_ColorId); return; } enum iColorId fg = run->color; @@ -2239,6 +2261,119 @@ static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) { unsetClip_Paint(&p); } +iDeclareType(PlayerUI) + +struct Impl_PlayerUI { + const iPlayer *player; + iRect bounds; + iRect playPauseRect; + iRect rewindRect; + iRect scrubberRect; + iRect menuRect; +}; + +static void init_PlayerUI(iPlayerUI *d, const iPlayer *player, iRect bounds) { + d->player = player; + d->bounds = bounds; + const int height = height_Rect(bounds); + d->playPauseRect = (iRect){ addX_I2(topLeft_Rect(bounds), gap_UI / 2), init1_I2(height) }; + d->rewindRect = (iRect){ topRight_Rect(d->playPauseRect), init1_I2(height) }; + d->menuRect = (iRect){ addX_I2(topRight_Rect(bounds), -height - gap_UI / 2), init1_I2(height) }; + d->scrubberRect = initCorners_Rect(topRight_Rect(d->rewindRect), bottomLeft_Rect(d->menuRect)); +} + +static void drawPlayerButton_(iPaint *p, iRect rect, const char *label) { + const iInt2 mouse = mouseCoord_Window(get_Window()); + const iBool isHover = contains_Rect(rect, mouse); + const iBool isPressed = isHover && (SDL_GetMouseState(NULL, NULL) & SDL_BUTTON_LEFT) != 0; + const int frame = (isPressed ? uiTextCaution_ColorId : isHover ? uiHeading_ColorId : uiAnnotation_ColorId); + iRect frameRect = shrunk_Rect(rect, init_I2(gap_UI / 2, gap_UI)); + drawRect_Paint(p, frameRect, frame); + if (isPressed) { + fillRect_Paint( + p, + adjusted_Rect(shrunk_Rect(frameRect, divi_I2(gap2_UI, 2)), zero_I2(), one_I2()), + frame); + } + const int fg = isPressed ? uiBackground_ColorId : frame; + drawCentered_Text(uiContent_FontId, frameRect, iTrue, fg, "%s", label); +} + +static int drawSevenSegmentTime_(iInt2 pos, int color, int align, int seconds) { /* returns width */ + const uint32_t sevenSegmentDigit = 0x1fbf0; + const int hours = seconds / 3600; + const int mins = (seconds / 60) % 60; + const int secs = seconds % 60; + const int font = uiLabel_FontId; + iString num; + init_String(&num); + if (hours) { + appendChar_String(&num, sevenSegmentDigit + (hours % 10)); + appendChar_String(&num, ':'); + } + appendChar_String(&num, sevenSegmentDigit + (mins / 10) % 10); + appendChar_String(&num, sevenSegmentDigit + (mins % 10)); + appendChar_String(&num, ':'); + appendChar_String(&num, sevenSegmentDigit + (secs / 10) % 10); + appendChar_String(&num, sevenSegmentDigit + (secs % 10)); + iInt2 size = advanceRange_Text(font, range_String(&num)); + if (align == right_Alignment) { + pos.x -= size.x; + } + drawRange_Text(font, pos, color, range_String(&num)); + deinit_String(&num); + return size.x; +} + +static void draw_PlayerUI(iPlayerUI *d, iPaint *p) { + const int playerBackground_ColorId = uiBackground_ColorId; + const int playerFrame_ColorId = uiSeparator_ColorId; + fillRect_Paint(p, d->bounds, playerBackground_ColorId); + drawRect_Paint(p, d->bounds, playerFrame_ColorId); + drawPlayerButton_(p, d->playPauseRect, isPaused_Player(d->player) ? "\U0001f782" : "\u23f8"); + drawPlayerButton_(p, d->rewindRect, "\u23ee"); + drawPlayerButton_(p, d->menuRect, "\U0001d362"); + const int hgt = lineHeight_Text(uiLabel_FontId); + const int yMid = mid_Rect(d->scrubberRect).y; + const float playTime = time_Player(d->player); + const float totalTime = duration_Player(d->player); + const int bright = uiHeading_ColorId; + const int dim = uiAnnotation_ColorId; + int leftWidth = drawSevenSegmentTime_( + init_I2(left_Rect(d->scrubberRect) + 2 * gap_UI, yMid - hgt / 2), + isPaused_Player(d->player) ? dim : bright, + left_Alignment, + iRound(playTime)); + int rightWidth = drawSevenSegmentTime_( + init_I2(right_Rect(d->scrubberRect) - 2 * gap_UI, yMid - hgt / 2), + dim, + right_Alignment, + iRound(totalTime)); + /* Scrubber. */ + const int s1 = left_Rect(d->scrubberRect) + leftWidth + 6 * gap_UI; + const int s2 = right_Rect(d->scrubberRect) - rightWidth - 6 * gap_UI; + const float normPos = totalTime > 0 ? playTime / totalTime : 0.0f; + const int part = (s2 - s1) * normPos; + drawHLine_Paint(p, init_I2(s1, yMid), part, bright); + drawHLine_Paint(p, init_I2(s1 + part, yMid), (s2 - s1) - part, dim); + draw_Text(uiLabel_FontId, + init_I2(s1 * (1.0f - normPos) + s2 * normPos - hgt / 3, yMid - hgt / 2), + bright, + "\u23fa"); +} + +static void drawAudioPlayers_DocumentWidget_(const iDocumentWidget *d, iPaint *p) { + const iRect docBounds = documentBounds_DocumentWidget_(d); + iConstForEach(PtrArray, i, &d->visiblePlayers) { + const iGmRun * run = i.ptr; + const iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->audioId); + const iRect rect = moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), -d->scrollY)); + iPlayerUI ui; + init_PlayerUI(&ui, plr, rect); + draw_PlayerUI(&ui, p); + } +} + static void draw_DocumentWidget_(const iDocumentWidget *d) { const iWidget *w = constAs_Widget(d); const iRect bounds = bounds_Widget(w); @@ -2297,7 +2432,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { } } endTarget_Paint(&ctx.paint); - fflush(stdout); +// fflush(stdout); } validate_VisBuf(visBuf); clear_PtrSet(d->invalidRuns); @@ -2314,6 +2449,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { render_GmDocument(d->doc, vis, drawMark_DrawContext_, &ctx); SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); } + drawAudioPlayers_DocumentWidget_(d, &ctx.paint); unsetClip_Paint(&ctx.paint); /* Fill the top and bottom, in case the document is short. */ if (yTop > top_Rect(bounds)) { -- cgit v1.2.3