1 //////////////////////////////////////////////////////////////////////////////
2 //
3 // Copyright (c) 2004-2021 musikcube team
4 //
5 // All rights reserved.
6 //
7 // Redistribution and use in source and binary forms, with or without
8 // modification, are permitted provided that the following conditions are met:
9 //
10 //    * Redistributions of source code must retain the above copyright notice,
11 //      this list of conditions and the following disclaimer.
12 //
13 //    * Redistributions in binary form must reproduce the above copyright
14 //      notice, this list of conditions and the following disclaimer in the
15 //      documentation and/or other materials provided with the distribution.
16 //
17 //    * Neither the name of the author nor the names of other contributors may
18 //      be used to endorse or promote products derived from this software
19 //      without specific prior written permission.
20 //
21 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
25 // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26 // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
27 // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29 // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 // POSSIBILITY OF SUCH DAMAGE.
32 //
33 //////////////////////////////////////////////////////////////////////////////
34 
35 #include "SndioOut.h"
36 
37 #include <musikcore/sdk/constants.h>
38 #include <musikcore/sdk/IPreferences.h>
39 #include <math.h>
40 #include <limits.h>
41 #include <iostream>
42 #include <functional>
43 
44 #define BUFFER_COUNT 16
45 #define ERROR(str) std::cerr << "SndioOut Error: " << str << "\n";
46 #define INFO(str) std::cerr << "SndioOut Info: " << str << "\n";
47 #define LOCK() std::unique_lock<std::mutex> lock(this->mutex);
48 #define WAIT() this->threadEvent.wait(lock);
49 #define NOTIFY() this->threadEvent.notify_all();
50 
51 #define STOP() \
52   if(started) { \
53     if (!sio_stop(handle)) { \
54       INFO("failed to stop sndio") \
55       quit = true; \
56       continue; \
57     } \
58     else { \
59       INFO("stopped handle") \
60       started = false; \
61    } \
62   } \
63   else { \
64     INFO("already stopped") \
65   }
66 
67 #define START() \
68   if(!started) { \
69     if (!sio_start(handle)) { \
70       INFO("failed to start sndio") \
71       quit = true; \
72       continue; \
73     } \
74     else { \
75       INFO("started handle") \
76       started = true; \
77    } \
78   } \
79   else { \
80     INFO("handle already started") \
81   }
82 
83 #define PREF_DEVICE_ID "device_id"
84 
85 using namespace musik::core::sdk;
86 
87 static char* deviceId = nullptr;
88 
SetPreferences(musik::core::sdk::IPreferences * prefs)89 extern "C" void SetPreferences(musik::core::sdk::IPreferences* prefs) {
90     size_t len = prefs->GetString(PREF_DEVICE_ID, nullptr, 0, "");
91     if (len > 1) {
92         delete[] deviceId;
93         deviceId = new char[len];
94         prefs->GetString(PREF_DEVICE_ID, deviceId, len, "");
95         prefs->Save();
96         INFO("setting deviceId to " + std::string(deviceId));
97     }
98 }
99 
SndioOut()100 SndioOut::SndioOut() {
101     INFO("---------- sndout.ctor ----------")
102     this->volume = 1.0f;
103     this->state = StateStopped;
104     this->latency = 0.0;
105     this->writeThread.reset(new std::thread(
106         std::bind(&SndioOut::WriteLoop, this)));
107 }
108 
~SndioOut()109 SndioOut::~SndioOut() {
110     this->PushCommand(Command::Quit);
111     INFO("joining thread")
112     this->writeThread->join();
113     INFO("thread finished")
114 }
115 
Release()116 void SndioOut::Release() {
117     delete this;
118 }
119 
PushCommand(Command command)120 void SndioOut::PushCommand(Command command) {
121     //INFO("PushCommand.start")
122     {
123         LOCK()
124         commands.push_back(command);
125     }
126     NOTIFY()
127     //INFO("PushCommand.end")
128 }
129 
Pause()130 void SndioOut::Pause() {
131     this->PushCommand(Command::Pause);
132 }
133 
Resume()134 void SndioOut::Resume() {
135     this->PushCommand(Command::Resume);
136 }
137 
SetVolume(double volume)138 void SndioOut::SetVolume(double volume) {
139     this->volume = volume;
140     this->PushCommand(Command::SetVolume);
141 }
142 
GetVolume()143 double SndioOut::GetVolume() {
144     return this->volume;
145 }
146 
Stop()147 void SndioOut::Stop() {
148     this->PushCommand(Command::Stop);
149     this->DiscardBuffers();
150 }
151 
Drain()152 void SndioOut::Drain() {
153     this->PushCommand(Command::Drain);
154 }
155 
GetDeviceList()156 IDeviceList* SndioOut::GetDeviceList() {
157     return nullptr;
158 }
159 
SetDefaultDevice(const char * deviceId)160 bool SndioOut::SetDefaultDevice(const char* deviceId) {
161     return false;
162 }
163 
GetDefaultDevice()164 IDevice* SndioOut::GetDefaultDevice() {
165     return nullptr;
166 }
167 
Play(IBuffer * buffer,IBufferProvider * provider)168 OutputState SndioOut::Play(IBuffer *buffer, IBufferProvider *provider) {
169     std::this_thread::yield();
170 
171     if (this->state != StatePlaying) {
172         return OutputState::InvalidState;
173     }
174 
175     {
176         LOCK()
177         if (this->CountBuffersWithProvider(provider) >= BUFFER_COUNT) {
178             return OutputState::BufferFull;
179         }
180         this->buffers.push_back(BufferContext{provider, buffer});
181     }
182 
183     NOTIFY()
184     return OutputState::BufferWritten;
185 }
186 
WriteLoop()187 void SndioOut::WriteLoop() {
188     bool started = false;
189     sio_hdl* handle = nullptr;
190     sio_par pars = { 0 };
191     short* pcm = nullptr;
192     long pcmSamples = 0;
193     float ditherState = 0.0;
194     bool quit = false;
195     BufferContext next {nullptr, nullptr};
196 
197     /* open the output device. we only do this once */
198     const char* device = (::deviceId && strlen(::deviceId)) ? deviceId : nullptr;
199     handle = sio_open(device, SIO_PLAY, 0);
200     if (handle == nullptr) {
201         INFO("failed to init device")
202         quit = true;
203     }
204     else {
205         if (!sio_start(handle)) {
206             INFO("device opened, but couldn't start")
207             quit = true;
208         }
209         else {
210             started = true;
211             INFO("device handle initialized")
212         }
213     }
214 
215     while (!quit) {
216         {
217             /* we wait until we have commands to process or samples to play */
218             LOCK()
219             while (!quit && !this->commands.size() &&
220                    (this->state != StatePlaying || !this->buffers.size()))
221             {
222                 //INFO("waiting")
223                 WAIT()
224                 //INFO("done waiting")
225             }
226 
227             if (quit) {
228                 continue;
229             }
230 
231             /* process commands */
232             for (auto command : this->commands) {
233                 switch (command) {
234                     case Command::Pause: {
235                         INFO("command.pause")
236                         STOP()
237                         this->state = StatePaused;
238                     } break;
239                     case Command::Resume: {
240                         INFO("command.resume")
241                         START()
242                         this->state = StatePlaying;
243                     } break;
244                     case Command::Stop: {
245                         INFO("command.stop")
246                         STOP()
247                         this->state = StateStopped;
248                     } break;
249                     case Command::SetVolume: {
250                         INFO("command.setvolume")
251                         if (handle) {
252                             sio_setvol(handle, lround(this->volume * SIO_MAXVOL));
253                         }
254                     } break;
255                     case Command::Drain: {
256                         INFO("command.drain")
257                         STOP()
258                         START()
259                     } break;
260                     case Command::Quit: {
261                         quit = true;
262                         continue;
263                     } break;
264                 }
265             }
266 
267             this->commands.clear();
268 
269             /* play the next buffer, if it exists */
270             if (started &&
271                 this->state == StatePlaying &&
272                 this->buffers.size())
273             {
274                 next = this->buffers.front();
275                 this->buffers.pop_front();
276             }
277             else {
278                 //INFO(std::string("state=") + std::to_string((int) this->state));
279                 //INFO(std::string("count=") + std::to_string((int) this->buffers.size()));
280                 continue;
281             }
282         }
283 
284         auto buffer = next.buffer;
285 
286         if (buffer) {
287             /* ensure our output params are setup properly based on this
288             buffer's params */
289             if (pars.pchan != buffer->Channels() ||
290                 pars.rate != buffer->SampleRate())
291             {
292                 INFO("updating output params")
293 
294                 STOP()
295 
296                 sio_initpar(&pars);
297                 pars.pchan = buffer->Channels();
298                 pars.rate = buffer->SampleRate();
299                 pars.sig = 1;
300                 pars.le = SIO_LE_NATIVE;
301                 pars.bits = 16;
302                 pars.appbufsz = (pars.rate * 250) / 1000;
303 
304                 if (!sio_setpar(handle, &pars)) {
305                     INFO("sio_setpar() failed");
306                     quit = true;
307                     continue;
308                 }
309 
310                 if (!sio_getpar(handle, &pars)) {
311                     INFO("sio_getpar() failed");
312                     quit = true;
313                     continue;
314                 }
315 
316                 START()
317 
318                 this->latency = (double) pars.bufsz / pars.pchan / pars.rate;
319             }
320 
321             /* allocate PCM buffer, if needed. most of the time we'll be
322             able to re-use the previously allocated one. */
323             long samples = buffer->Samples();
324             if (!pcm || samples > pcmSamples) {
325                 delete[] pcm;
326                 pcm = new short[samples];
327                 pcmSamples = samples;
328             }
329 
330             /* convert to 16-bit PCM */
331             float* src = buffer->BufferPointer();
332             short* dst = pcm;
333             for (long i = 0; i < samples; i++) {
334                 float sample = *src;
335                 if (sample > 1.0f) sample = 1.0f;
336                 if (sample < -1.0f) sample = -1.0f;
337                 sample *= SHRT_MAX;
338 
339                 /* triangle (high pass) dither, based on Audacity's
340                 implementation */
341                 float r = (rand() / (float) RAND_MAX - 0.5f);
342                 sample = sample + r - ditherState;
343                 ditherState = r;
344 
345                 *dst = sample;
346                 ++dst; ++src;
347             }
348 
349             /* write the entire output buffer. this may require multiple passes;
350             that's ok, just loop until we're done */
351             char* data = (char*) pcm;
352             size_t dataLength = samples * sizeof(short);
353             size_t totalWritten = 0;
354 
355             {
356 //              LOCK()
357 //              INFO("start write")
358                 while (totalWritten < dataLength) {
359                     size_t remaining = dataLength - totalWritten;
360                     size_t written = sio_write(handle, data, remaining);
361                     if (written == 0) { INFO("failed to write!") break; }
362 //                  else { std::cerr << "wrote " << written << " of " << remaining << " bytes\n"; }
363                     totalWritten += written;
364                     data += written;
365                 }
366 //              INFO("end write")
367             }
368 
369             next.provider->OnBufferProcessed(buffer);
370         }
371     }
372 
373     /* done, free remaining buffers and close the handle */
374     if (handle) {
375         INFO("closing")
376         sio_close(handle);
377         handle = nullptr;
378     }
379 
380     this->DiscardBuffers();
381 
382     delete[] pcm;
383 }
384 
Latency()385 double SndioOut::Latency() {
386     return this->latency;
387 }
388 
DiscardBuffers()389 void SndioOut::DiscardBuffers() {
390     std::list<BufferContext> toNotify;
391     {
392         LOCK()
393         std::swap(toNotify, this->buffers);
394     }
395 
396     for (auto& it : toNotify) {
397         it.provider->OnBufferProcessed(it.buffer);
398     }
399 }
400 
CountBuffersWithProvider(IBufferProvider * provider)401 size_t SndioOut::CountBuffersWithProvider(IBufferProvider* provider) {
402     size_t count = 0;
403     for (auto& it : this->buffers) {
404         if (it.provider == provider) {
405             ++count;
406         }
407     }
408     return count;
409 }
410 
411