1 /***
2     This file is part of snapcast
3     Copyright (C) 2014-2020  Johannes Pohl
4 
5     This program is free software: you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation, either version 3 of the License, or
8     (at your option) any later version.
9 
10     This program is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU General Public License for more details.
14 
15     You should have received a copy of the GNU General Public License
16     along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 ***/
18 
19 #include <cmath>
20 #include <iostream>
21 
22 #ifdef WINDOWS
23 #include <cstdlib>
24 #else
25 #pragma GCC diagnostic push
26 #pragma GCC diagnostic ignored "-Wunused-result"
27 #pragma GCC diagnostic ignored "-Wunused-parameter"
28 #pragma GCC diagnostic ignored "-Wmissing-braces"
29 #include <boost/process/args.hpp>
30 #include <boost/process/child.hpp>
31 #include <boost/process/exe.hpp>
32 #pragma GCC diagnostic pop
33 #endif
34 
35 #include "common/aixlog.hpp"
36 #include "common/snap_exception.hpp"
37 #include "common/str_compat.hpp"
38 #include "common/utils/string_utils.hpp"
39 #include "player.hpp"
40 
41 
42 using namespace std;
43 
44 namespace player
45 {
46 
47 static constexpr auto LOG_TAG = "Player";
48 
Player(boost::asio::io_context & io_context,const ClientSettings::Player & settings,std::shared_ptr<Stream> stream)49 Player::Player(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr<Stream> stream)
50     : io_context_(io_context), active_(false), stream_(stream), settings_(settings), volume_(1.0), muted_(false), volCorrection_(1.0)
51 {
52     string sharing_mode;
53     switch (settings_.sharing_mode)
54     {
55         case ClientSettings::SharingMode::unspecified:
56             sharing_mode = "unspecified";
57             break;
58         case ClientSettings::SharingMode::exclusive:
59             sharing_mode = "exclusive";
60             break;
61         case ClientSettings::SharingMode::shared:
62             sharing_mode = "shared";
63             break;
64     }
65 
66     auto not_empty = [](const std::string& value) -> std::string {
67         if (!value.empty())
68             return value;
69         else
70             return "<none>";
71     };
72     LOG(INFO, LOG_TAG) << "Player name: " << not_empty(settings_.player_name) << ", device: " << not_empty(settings_.pcm_device.name)
73                        << ", description: " << not_empty(settings_.pcm_device.description) << ", idx: " << settings_.pcm_device.idx
74                        << ", sharing mode: " << sharing_mode << ", parameters: " << not_empty(settings.parameter) << "\n";
75 
76     string mixer;
77     switch (settings_.mixer.mode)
78     {
79         case ClientSettings::Mixer::Mode::hardware:
80             mixer = "hardware";
81             break;
82         case ClientSettings::Mixer::Mode::software:
83             mixer = "software";
84             break;
85         case ClientSettings::Mixer::Mode::script:
86             mixer = "script";
87             break;
88         case ClientSettings::Mixer::Mode::none:
89             mixer = "none";
90             break;
91     }
92     LOG(INFO, LOG_TAG) << "Mixer mode: " << mixer << ", parameters: " << not_empty(settings_.mixer.parameter) << "\n";
93     LOG(INFO, LOG_TAG) << "Sampleformat: " << (settings_.sample_format.isInitialized() ? settings_.sample_format.toString() : stream->getFormat().toString())
94                        << ", stream: " << stream->getFormat().toString() << "\n";
95 }
96 
97 
~Player()98 Player::~Player()
99 {
100     stop();
101 }
102 
103 
start()104 void Player::start()
105 {
106     active_ = true;
107     if (needsThread())
108         playerThread_ = thread(&Player::worker, this);
109 
110     // If hardware mixer is used, send the initial volume to the server, because this is
111     // the volume that is configured by the user on his local device, so we shouldn't change it
112     // on client start up
113     // if (settings_.mixer.mode == ClientSettings::Mixer::Mode::hardware)
114     // {
115     //     if (getHardwareVolume(volume_, muted_))
116     //     {
117     //         LOG(DEBUG, LOG_TAG) << "Volume: " << volume_ << ", muted: " << muted_ << "\n";
118     //         notifyVolumeChange(volume_, muted_);
119     //     }
120     // }
121 }
122 
123 
stop()124 void Player::stop()
125 {
126     if (active_)
127     {
128         active_ = false;
129         if (playerThread_.joinable())
130             playerThread_.join();
131     }
132 }
133 
134 
worker()135 void Player::worker()
136 {
137 }
138 
139 
setHardwareVolume(double volume,bool muted)140 void Player::setHardwareVolume(double volume, bool muted)
141 {
142     std::ignore = volume;
143     std::ignore = muted;
144     throw SnapException("Failed to set hardware mixer volume: not supported");
145 }
146 
147 
getHardwareVolume(double & volume,bool & muted)148 bool Player::getHardwareVolume(double& volume, bool& muted)
149 {
150     std::ignore = volume;
151     std::ignore = muted;
152     throw SnapException("Failed to get hardware mixer volume: not supported");
153 }
154 
155 
adjustVolume(char * buffer,size_t frames)156 void Player::adjustVolume(char* buffer, size_t frames)
157 {
158     double volume = volCorrection_;
159     // apply volume changes only for software mixer
160     // for any other mixer, we might still have to apply the volCorrection_
161     if (settings_.mixer.mode == ClientSettings::Mixer::Mode::software)
162     {
163         volume = muted_ ? 0. : volume_;
164         volume *= volCorrection_;
165     }
166 
167     if (volume != 1.0)
168     {
169         const SampleFormat& sampleFormat = stream_->getFormat();
170         if (sampleFormat.sampleSize() == 1)
171             adjustVolume<int8_t>(buffer, frames * sampleFormat.channels(), volume);
172         else if (sampleFormat.sampleSize() == 2)
173             adjustVolume<int16_t>(buffer, frames * sampleFormat.channels(), volume);
174         else if (sampleFormat.sampleSize() == 4)
175             adjustVolume<int32_t>(buffer, frames * sampleFormat.channels(), volume);
176     }
177 }
178 
179 
180 // https://cgit.freedesktop.org/pulseaudio/pulseaudio/tree/src/pulse/volume.c#n260
181 // http://www.robotplanet.dk/audio/audio_gui_design/
182 // https://lists.linuxaudio.org/pipermail/linux-audio-dev/2009-May/thread.html#22198
setVolume_poly(double volume,double exp)183 void Player::setVolume_poly(double volume, double exp)
184 {
185     volume_ = std::pow(volume, exp);
186     LOG(DEBUG, LOG_TAG) << "setVolume poly with exp " << exp << ": " << volume << " => " << volume_ << "\n";
187 }
188 
189 
190 // http://stackoverflow.com/questions/1165026/what-algorithms-could-i-use-for-audio-volume-level
setVolume_exp(double volume,double base)191 void Player::setVolume_exp(double volume, double base)
192 {
193     //	double base = M_E;
194     //	double base = 10.;
195     volume_ = (pow(base, volume) - 1) / (base - 1);
196     LOG(DEBUG, LOG_TAG) << "setVolume exp with base " << base << ": " << volume << " => " << volume_ << "\n";
197 }
198 
199 
setVolume(double volume,bool mute)200 void Player::setVolume(double volume, bool mute)
201 {
202     volume_ = volume;
203     muted_ = mute;
204     if (settings_.mixer.mode == ClientSettings::Mixer::Mode::hardware)
205     {
206         setHardwareVolume(volume, mute);
207     }
208     else if (settings_.mixer.mode == ClientSettings::Mixer::Mode::software)
209     {
210         string param;
211         string mode = utils::string::split_left(settings_.mixer.parameter, ':', param);
212         double dparam = -1.;
213         if (!param.empty())
214         {
215             try
216             {
217                 dparam = cpt::stod(param);
218                 if (dparam < 0)
219                     throw SnapException("must be a positive number");
220             }
221             catch (const std::exception& e)
222             {
223                 throw SnapException("Invalid mixer param: " + param + ", error: " + string(e.what()));
224             }
225         }
226         if (mode == "poly")
227             setVolume_poly(volume, (dparam < 0) ? 3. : dparam);
228         else
229             setVolume_exp(volume, (dparam < 0) ? 10. : dparam);
230     }
231     else if (settings_.mixer.mode == ClientSettings::Mixer::Mode::script)
232     {
233         try
234         {
235 #ifdef WINDOWS
236             string cmd = settings_.mixer.parameter + " --volume " + cpt::to_string(volume) + " --mute " + (mute ? "true" : "false");
237             std::system(cmd.c_str());
238 #else
239             using namespace boost::process;
240             child c(exe = settings_.mixer.parameter, args = {"--volume", cpt::to_string(volume), "--mute", mute ? "true" : "false"});
241             c.detach();
242 #endif
243         }
244         catch (const std::exception& e)
245         {
246             LOG(ERROR, LOG_TAG) << "Failed to run script '" + settings_.mixer.parameter + "', error: " << e.what() << "\n";
247         }
248     }
249 }
250 
251 } // namespace player
252