URY playd
C++ minimalist audio player
player.cpp
Go to the documentation of this file.
1 // This file is part of playd.
2 // playd is licensed under the MIT licence: see LICENSE.txt.
3 
10 #include <cassert>
11 #include <cstdint>
12 #include <sstream>
13 #include <stdexcept>
14 #include <string>
15 #include <vector>
16 
17 #include "audio/audio_sink.hpp"
18 #include "audio/audio_source.hpp"
19 #include "audio/audio.hpp"
20 #include "errors.hpp"
21 #include "response.hpp"
22 #include "messages.h"
23 #include "player.hpp"
24 
25 Player::Player(int device_id, SinkFn sink, std::map<std::string, SourceFn> sources)
26  : device_id(device_id),
27  sink(std::move(sink)),
28  sources(std::move(sources)),
29  file(std::make_unique<NoAudio>()),
30  dead(false),
31  io(nullptr),
32  last_pos(0)
33 {
34 }
35 
37 {
38  this->io = &io;
39 }
40 
42 {
43  assert(this->file != nullptr);
44  auto as = this->file->Update();
45 
47  if (as == Audio::State::PLAYING) {
48  // Since the audio is currently playing, the position may have
49  // advanced since last update. So we need to update it.
50  auto pos = this->file->Position();
51  if (this->CanAnnounceTime(pos)) {
54  .AddArg(std::to_string(pos)));
55  }
56  }
57 
58  return !this->dead;
59 }
60 
61 //
62 // Commands
63 //
64 
65 Response Player::Dump(size_t id, const std::string &tag) const
66 {
67  if (this->dead) return Response::Failure(tag, MSG_CMD_PLAYER_CLOSING);
68 
69  this->DumpState(id, tag);
70 
71  // This information won't exist if there is no file.
72  if (this->file->CurrentState() != Audio::State::NONE) {
73  auto file = this->file->File();
74  this->Respond(
75  id, Response(tag, Response::Code::FLOAD).AddArg(file));
76 
77  auto pos = this->file->Position();
78  this->Respond(id, Response(tag, Response::Code::POS)
79  .AddArg(std::to_string(pos)));
80  }
81 
82  return Response::Success(tag);
83 }
84 
85 Response Player::Eject(const std::string &tag)
86 {
87  if (this->dead) return Response::Failure(tag, MSG_CMD_PLAYER_CLOSING);
88 
89  // Silently ignore ejects on ejected files.
90  // Concurrently speaking, this should be fine, as we are the only
91  // thread that can eject or un-eject files.
92  if (this->file->CurrentState() == Audio::State::NONE) {
93  return Response::Success(tag);
94  }
95 
96  assert(this->file != nullptr);
97  this->file = std::make_unique<NoAudio>();
98 
99  this->DumpState(0, tag);
100 
101  return Response::Success(tag);
102 }
103 
104 Response Player::End(const std::string &tag)
105 {
106  if (this->dead) return Response::Failure(tag, MSG_CMD_PLAYER_CLOSING);
107 
108  // Let upstream know that the file ended by itself.
109  // This is needed for auto-advancing playlists, etc.
111 
112  this->SetPlaying(tag, false);
113 
114  // Rewind the file back to the start. We can't use Player::Pos() here
115  // in case End() is called from Pos(); a seek failure could start an
116  // infinite loop.
117  this->PosRaw(Response::NOREQUEST, 0);
118 
119  return Response::Success(tag);
120 }
121 
122 Response Player::Load(const std::string &tag, const std::string &path)
123 {
124  if (this->dead) return Response::Failure(tag, MSG_CMD_PLAYER_CLOSING);
125  if (path.empty()) return Response::Invalid(tag, MSG_LOAD_EMPTY_PATH);
126 
127  assert(this->file != nullptr);
128 
129  // Bin the current file as soon as possible.
130  // This ensures that we don't have any situations where two files are
131  // contending over resources, or the current file spends a second or
132  // two flushing its remaining audio.
133  this->Eject(Response::NOREQUEST);
134 
135  try {
136  this->file = this->LoadRaw(path);
137  } catch (FileError &e) {
138  // File errors aren't fatal, so catch them here.
139  return Response::Failure(tag, e.Message());
140  }
141 
142  assert(this->file != nullptr);
143  this->last_pos = 0;
144 
145  // A load will change all of the player's state in one go,
146  // so just send a Dump() instead of writing out all of the responses
147  // here.
148  // Don't take the response from here, though, because it has the wrong
149  // tag.
150  this->Dump(0, Response::NOREQUEST);
151 
152  return Response::Success(tag);
153 }
154 
155 Response Player::Pos(const std::string &tag, const std::string &pos_str)
156 {
157  if (this->dead) return Response::Failure(tag, MSG_CMD_PLAYER_CLOSING);
158 
159  std::uint64_t pos = 0;
160  try {
161  pos = PosParse(pos_str);
162  } catch (SeekError &e) {
163  // Seek errors here are a result of clients sending weird times.
164  // Thus, we tell them off.
165  return Response::Invalid(tag, e.Message());
166  }
167 
168  try {
169  this->PosRaw(tag, pos);
170  } catch (NoAudioError) {
172  } catch (SeekError) {
173  // Seek failures here are a result of the decoder not liking the
174  // seek position (usually because it's outside the audio file!).
175  // Thus, unlike above, we try to recover.
176 
177  Debug() << "Seek failure" << std::endl;
178 
179  // Make it look to the client as if the seek ran off the end of
180  // the file.
181  this->End(tag);
182  }
183 
184  // If we've made it all the way down here, we deserve to succeed.
185  return Response::Success(tag);
186 }
187 
188 Response Player::SetPlaying(const std::string &tag, bool playing)
189 {
190  if (this->dead) return Response::Failure(tag, MSG_CMD_PLAYER_CLOSING);
191 
192  // Why is SetPlaying not split between Start() and Stop()?, I hear the
193  // best practices purists amongst you say. Quite simply, there is a
194  // large amount of fiddly exception boilerplate here that would
195  // otherwise be duplicated between the two methods.
196 
197  assert(this->file != nullptr);
198 
199  try {
200  this->file->SetPlaying(playing);
201  } catch (NoAudioError &e) {
202  return Response::Invalid(tag, e.Message());
203  }
204 
205  this->DumpState(0, Response::NOREQUEST);
206 
207  return Response::Success(tag);
208 }
209 
210 Response Player::Quit(const std::string &tag)
211 {
212  if (this->dead) return Response::Failure(tag, MSG_CMD_PLAYER_CLOSING);
213 
214  this->Eject(tag);
215  this->dead = true;
216  return Response::Success(tag);
217 }
218 
219 //
220 // Command implementations
221 //
222 
223 /* static */ std::uint64_t Player::PosParse(const std::string &pos_str)
224 {
225  size_t cpos = 0;
226 
227  // Try and see if this position string is negative.
228  // Cheap and easy way: see if it has '-'.
229  // This means we don't need to skip whitespace first, with no loss
230  // of suction: no valid position string will contain '-'.
231  if (pos_str.find('-') != std::string::npos) {
233  }
234 
235 
236  std::uint64_t pos;
237  try {
238  pos = std::stoull(pos_str, &cpos);
239  } catch (...) {
241  }
242 
243  // cpos will point to the first character in pos that wasn't a number.
244  // We don't want any such characters here, so bail if the position isn't
245  // at the end of the string.
246  auto sl = pos_str.length();
247  if (cpos != sl) throw SeekError(MSG_SEEK_INVALID_VALUE);
248 
249  return pos;
250 }
251 
252 void Player::PosRaw(const std::string &tag, std::uint64_t pos)
253 {
254  assert(this->file != nullptr);
255 
256  this->file->SetPosition(pos);
257 
258  // This is required to make CanAnnounceTime() continue working.
259  this->last_pos = pos / 1000 / 1000;
260 
261  this->Respond(0, Response(tag, Response::Code::POS)
262  .AddArg(std::to_string(pos)));
263 }
264 
265 void Player::DumpState(size_t id, const std::string &tag) const
266 {
268 
269  switch (this->file->CurrentState()) {
271  code = Response::Code::END;
272  break;
273  case Audio::State::NONE:
274  code = Response::Code::EJECT;
275  break;
277  code = Response::Code::PLAY;
278  break;
280  code = Response::Code::STOP;
281  break;
282  default:
283  // Just don't dump anything in this case.
284  return;
285  }
286 
287  this->Respond(id, Response(tag, code));
288 }
289 
290 void Player::Respond(int id, Response rs) const
291 {
292  if (this->io != nullptr) this->io->Respond(id, rs);
293 }
294 
295 bool Player::CanAnnounceTime(std::uint64_t micros)
296 {
297  std::uint64_t secs = micros / 1000 / 1000;
298 
299  // We can announce if the last announced pos was in a previous second.
300  bool announce = this->last_pos < secs;
301  if (announce) this->last_pos = secs;
302 
303  return announce;
304 }
305 
306 std::unique_ptr<Audio> Player::LoadRaw(const std::string &path) const
307 {
308  std::unique_ptr<AudioSource> source = this->LoadSource(path);
309  assert(source != nullptr);
310 
311  auto sink = this->sink(*source, this->device_id);
312  return std::make_unique<PipeAudio>(std::move(source), std::move(sink));
313 }
314 
315 std::unique_ptr<AudioSource> Player::LoadSource(const std::string &path) const
316 {
317  size_t extpoint = path.find_last_of('.');
318  std::string ext = path.substr(extpoint + 1);
319 
320  auto ibuilder = this->sources.find(ext);
321  if (ibuilder == this->sources.end()) {
322  throw FileError("Unknown file format: " + ext);
323  }
324 
325  return (ibuilder->second)(path);
326 }
Declarations of the playd Error exception set.
Declaration of the AudioSource class.
The Audio is currently playing.
bool Update()
Instructs the Player to perform a cycle of work.
Definition: player.cpp:41
static std::uint64_t PosParse(const std::string &pos_str)
Parses pos_str as a seek timestamp.
Definition: player.cpp:223
A response.
Definition: response.hpp:23
const std::string MSG_CMD_NEEDS_LOADED
Message shown when a command that works only when a file is loaded is fired when there isn&#39;t anything...
Definition: messages.h:42
std::function< std::unique_ptr< AudioSink >(const AudioSource &, int)> SinkFn
Type for functions that construct sinks.
Definition: player.hpp:36
Response Load(const std::string &tag, const std::string &path)
Loads a file.
Definition: player.cpp:122
const std::string MSG_LOAD_EMPTY_PATH
Message shown when one tries to Load an empty path.
Definition: messages.h:78
The Audio has ended and can&#39;t play without a seek.
void PosRaw(const std::string &tag, std::uint64_t pos)
Performs an actual seek.
Definition: player.cpp:252
Constant human-readable messages used within playd.
Declaration of the Audio class.
void Respond(int id, Response rs) const
Outputs a response, if there is a ResponseSink attached.
Definition: player.cpp:290
Response Quit(const std::string &tag)
Quits playd.
Definition: player.cpp:210
virtual void Respond(size_t id, const Response &response) const
Outputs a response.
Definition: response.cpp:97
STL namespace.
const ResponseSink * io
The sink for responses.
Definition: player.hpp:146
const std::string MSG_CMD_PLAYER_CLOSING
Message shown when a command is sent to a closing Player.
Definition: messages.h:57
An Error signifying that playd can&#39;t read a file.
Definition: errors.hpp:75
static const std::string NOREQUEST
The tag for unsolicited messages (not from responses).
Definition: response.hpp:27
const std::string & Message() const
The human-readable message for this error.
Definition: errors.cpp:16
static Response Success(const std::string &tag)
Shortcut for constructing a final response to a successful request.
Definition: response.cpp:49
bool CanAnnounceTime(std::uint64_t micros)
Determines whether we can broadcast a POS response.
Definition: player.cpp:295
There is no Audio.
Abstract class for anything that can be sent a response.
Definition: response.hpp:117
bool dead
Whether the Player is closing.
Definition: player.hpp:145
Response Eject(const std::string &tag)
Ejects the current loaded song, if any.
Definition: player.cpp:85
int device_id
The sink&#39;s device ID.
Definition: player.hpp:141
std::unique_ptr< AudioSource > LoadSource(const std::string &path) const
Loads a file, creating an AudioSource.
Definition: player.cpp:315
SinkFn sink
The sink create function.
Definition: player.hpp:142
The loaded file just changed.
std::map< std::string, SourceFn > sources
The file formats map.
Definition: player.hpp:143
The loaded file is playing.
The loaded file just ended.
The loaded file just ejected.
static Response Failure(const std::string &tag, const std::string &msg)
Shortcut for constructing a final response to a failed request.
Definition: response.cpp:62
Player(int device_id, SinkFn sink, std::map< std::string, SourceFn > sources)
Constructs a Player.
Definition: player.cpp:25
A dummy Audio implementation representing a lack of file.
Definition: audio.hpp:122
Declaration of the AudioSink class.
Response Pos(const std::string &tag, const std::string &pos_str)
Seeks to a given position in the current file.
Definition: player.cpp:155
std::unique_ptr< Audio > LoadRaw(const std::string &path) const
Loads a file, creating an Audio for it.
Definition: player.cpp:306
void DumpState(size_t id, const std::string &tag) const
Emits a response for the current audio state to the sink.
Definition: player.cpp:265
std::uint64_t last_pos
The last-sent position.
Definition: player.hpp:147
Declaration of the Player class, and associated types.
An Error signifying that playd can&#39;t seek in a file.
Definition: errors.hpp:90
Response Dump(size_t id, const std::string &tag) const
Dumps the current player state to the given ID.
Definition: player.cpp:65
Declaration of classes pertaining to responses to the client.
Response SetPlaying(const std::string &tag, bool playing)
Tells the audio file to start or stop playing.
Definition: player.cpp:188
Class for telling the human what playd is doing.
Definition: errors.hpp:133
std::unique_ptr< Audio > file
The loaded audio file.
Definition: player.hpp:144
Response End(const std::string &tag)
Ends a file, stopping and rewinding.
Definition: player.cpp:104
const std::string MSG_SEEK_INVALID_VALUE
Message shown when a seek command has an invalid time value.
Definition: messages.h:98
Code
Enumeration of all possible response codes.
Definition: response.hpp:35
The loaded file has stopped.
void SetIo(ResponseSink &io)
Sets the ResponseSink to which this Player shall send responses.
Definition: player.cpp:36
An Error signifying that no audio is loaded.
Definition: errors.hpp:120
Server sending current song time.
The Audio has been stopped, or not yet played.
static Response Invalid(const std::string &tag, const std::string &msg)
Shortcut for constructing a final response to a invalid request.
Definition: response.cpp:56