@startuml !theme plain hide empty description title Placed audio player - command loop and worker detail skinparam backgroundColor transparent skinparam shadowing false skinparam roundcorner 14 skinparam ArrowThickness 1.2 skinparam DefaultFontName "DejaVu Sans" skinparam DefaultFontSize 13 skinparam state { BackgroundColor #F8F8F8 BorderColor #555555 FontColor #222222 StartColor #555555 EndColor #555555 BackgroundColor<> #DCEEFF BorderColor<> #3A7FC4 FontColor<> #123A63 BackgroundColor<> #E3F6E3 BorderColor<> #3C9D40 FontColor<> #1E5C22 BackgroundColor<> #FFE8CC BorderColor<> #D8842A FontColor<> #7A4A12 BackgroundColor<> #FFE5E5 BorderColor<> #CC3333 FontColor<> #7A1F1F BackgroundColor<> #F2F2F2 BorderColor<> #888888 FontColor<> #444444 } state "Command loop:\nreplace active worker" as A <> { [*] -down-> CurrentWorkerActive state "Current worker active" as CurrentWorkerActive <> state "Interrupt requested" as InterruptRequested <> state "Waiting for worker" as WaitingForWorker <> state "Starting new worker" as StartingNewWorker <> state "New worker active" as NewWorkerActive <> InterruptRequested : entry / feed-interrupted := #t InterruptRequested : entry / ao-clear-async InterruptRequested : entry / player-state := stopped InterruptRequested : entry / audio-stop WaitingForWorker : do / wait until feeding-audio = #f StartingNewWorker : entry / thread-wait StartingNewWorker : entry / audio-open StartingNewWorker : entry / player-state := playing StartingNewWorker : entry / spawn worker CurrentWorkerActive -down-> InterruptRequested : open(new) InterruptRequested -down-> WaitingForWorker WaitingForWorker -down-> StartingNewWorker : ready StartingNewWorker -down-> NewWorkerActive NewWorkerActive -down-> [*] } state "Worker thread lifecycle" as B <> { [*] -down-> WorkerIdle state "WorkerIdle" as WorkerIdle <> state "WorkerExited" as WorkerExited <> state "WorkerFailed" as WorkerFailed <> WorkerIdle -down-> C : open(file)\n/ audio-open\nspawn worker state "Worker active" as C <> { C : entry / feeding-audio := #t C : exit / feeding-audio := #f [*] -right-> Reading state "Reading" as Reading <> state "DrainingAO" as DrainingAO <> state "MarkStopped" as MarkStopped <> state "WorkerDone" as WorkerDone <> Reading : do / audio-read Reading : pause #t / ao-pause #t and wait Reading : pause #f / ao-pause #f DrainingAO : do / wait until AO queue drains DrainingAO : pause #t / ao-pause #t DrainingAO : pause #f / ao-pause #f Reading -right-> DrainingAO : audio-read returns\n[not feed-interrupted]\n/ emit audio-done DrainingAO -right-> MarkStopped : [AO queue empty\nand file-id is current] MarkStopped -right-> WorkerDone : / player-state := stopped WorkerDone -right-> [*] Reading -down-> WorkerDone : [feed-interrupted]\n/ feed-interrupted := #f DrainingAO -down-> WorkerDone : [AO closed,\nqueue grows,\nor old file-id] note bottom of DrainingAO The file-id check prevents an old worker from stopping a newly opened file. end note } C -down-> WorkerExited : worker exits C -down-> WorkerFailed : exception\n/ emit exception\nplayer-state := stopped WorkerExited -down-> [*] WorkerFailed -down-> [*] } A -[hidden]right-> B @enduml