|
1
|
1 #include "foobar2000-sdk-pch.h"
|
|
|
2 #include "output.h"
|
|
|
3 #include "audio_chunk_impl.h"
|
|
|
4 #include "dsp.h"
|
|
|
5 #include "resampler.h"
|
|
|
6
|
|
|
7 pfc::string8 output_entry::get_device_name( const GUID & deviceID ) {
|
|
|
8 pfc::string8 temp;
|
|
|
9 if (!get_device_name(deviceID, temp)) temp = "[unknown device]";
|
|
|
10 return temp;
|
|
|
11 }
|
|
|
12
|
|
|
13 namespace {
|
|
|
14 class output_device_enum_callback_getname : public output_device_enum_callback {
|
|
|
15 public:
|
|
|
16 output_device_enum_callback_getname( const GUID & wantID, pfc::string_base & strOut ) : m_strOut(strOut), m_wantID(wantID) {}
|
|
|
17 void on_device(const GUID & p_guid,const char * p_name,unsigned p_name_length) {
|
|
|
18 if (!m_got && p_guid == m_wantID) {
|
|
|
19 m_strOut.set_string(p_name, p_name_length);
|
|
|
20 m_got = true;
|
|
|
21 }
|
|
|
22 }
|
|
|
23 bool m_got = false;
|
|
|
24 pfc::string_base & m_strOut;
|
|
|
25 const GUID m_wantID;
|
|
|
26 };
|
|
|
27
|
|
|
28 }
|
|
|
29
|
|
|
30 bool output_entry::get_device_name( const GUID & deviceID, pfc::string_base & out ) {
|
|
|
31 output_device_enum_callback_getname cb(deviceID, out);
|
|
|
32 this->enum_devices(cb);
|
|
|
33 return cb.m_got;
|
|
|
34 }
|
|
|
35
|
|
|
36 bool output_entry::g_find( const GUID & outputID, output_entry::ptr & outObj ) {
|
|
|
37 for (auto obj : enumerate()) {
|
|
|
38 if (obj->get_guid() == outputID) {
|
|
|
39 outObj = obj; return true;
|
|
|
40 }
|
|
|
41 }
|
|
|
42 return false;
|
|
|
43 }
|
|
|
44
|
|
|
45 output_entry::ptr output_entry::g_find( const GUID & outputID ) {
|
|
|
46 output_entry::ptr ret;
|
|
|
47 if (!g_find( outputID, ret ) ) throw exception_output_module_not_found();
|
|
|
48 return ret;
|
|
|
49 }
|
|
|
50
|
|
|
51
|
|
|
52 bool output::is_progressing_() {
|
|
|
53 output_v4::ptr v4;
|
|
|
54 if ( v4 &= this ) return v4->is_progressing();
|
|
|
55 return true;
|
|
|
56 }
|
|
|
57
|
|
|
58 size_t output::update_v2_() {
|
|
|
59 output_v4::ptr v4;
|
|
|
60 if ( v4 &= this ) return v4->update_v2();
|
|
|
61 bool bReady = false;
|
|
|
62 this->update(bReady);
|
|
|
63 return bReady ? SIZE_MAX : 0;
|
|
|
64 }
|
|
|
65
|
|
|
66 pfc::eventHandle_t output::get_trigger_event_() {
|
|
|
67 output_v4::ptr v4;
|
|
|
68 if ( v4 &= this ) return v4->get_trigger_event();
|
|
|
69 return pfc::eventInvalid;
|
|
|
70 }
|
|
|
71
|
|
|
72 size_t output::process_samples_v2_(const audio_chunk& c) {
|
|
|
73 output_v6::ptr v6;
|
|
|
74 if (v6 &= this) return v6->process_samples_v2(c);
|
|
|
75 this->process_samples(c);
|
|
|
76 return c.get_sample_count();
|
|
|
77 }
|
|
|
78
|
|
|
79 void output_impl::on_flush_internal() {
|
|
|
80 m_eos = false; m_sent_force_play = false;
|
|
|
81 m_incoming_ptr = 0;
|
|
|
82 m_incoming.set_size(0);
|
|
|
83 }
|
|
|
84
|
|
|
85 void output_impl::flush() {
|
|
|
86 on_flush_internal();
|
|
|
87 on_flush();
|
|
|
88 }
|
|
|
89
|
|
|
90 void output_impl::flush_changing_track() {
|
|
|
91 on_flush_internal();
|
|
|
92 on_flush_changing_track();
|
|
|
93 }
|
|
|
94
|
|
|
95 void output_impl::update(bool & p_ready) {
|
|
|
96 p_ready = update_v2() > 0;
|
|
|
97 }
|
|
|
98 size_t output_impl::update_v2() {
|
|
|
99
|
|
|
100 // Clear preemptively
|
|
|
101 m_can_write = 0;
|
|
|
102
|
|
|
103 on_update();
|
|
|
104
|
|
|
105 // No data yet, nothing to do, want data, can't signal how much because we don't know the format
|
|
|
106 if (!m_incoming_spec.is_valid()) return SIZE_MAX;
|
|
|
107
|
|
|
108 // First chunk in or format change
|
|
|
109 if (m_incoming_spec != m_active_spec) {
|
|
|
110 if (get_latency_samples() == 0) {
|
|
|
111 // Ready for new format
|
|
|
112 m_sent_force_play = false;
|
|
|
113 open(m_incoming_spec);
|
|
|
114 m_active_spec = m_incoming_spec;
|
|
|
115 } else {
|
|
|
116 // Previous format still playing, accept no more data
|
|
|
117 this->send_force_play();
|
|
|
118 return 0;
|
|
|
119 }
|
|
|
120 }
|
|
|
121
|
|
|
122 // opened for m_incoming_spec stream
|
|
|
123
|
|
|
124 // Store & update m_can_write on our end
|
|
|
125 // We don't know what can_write_samples() actually does, could be expensive, avoid calling it repeatedly
|
|
|
126 m_can_write = this->can_write_samples();
|
|
|
127
|
|
|
128 if (m_incoming_ptr < m_incoming.get_size()) {
|
|
|
129 t_size delta = pfc::min_t(m_incoming.get_size() - m_incoming_ptr, m_can_write * m_incoming_spec.chanCount);
|
|
|
130 if (delta > 0) {
|
|
|
131 PFC_ASSERT(!m_sent_force_play);
|
|
|
132 write(audio_chunk_temp_impl(m_incoming.get_ptr() + m_incoming_ptr, delta / m_incoming_spec.chanCount, m_incoming_spec.sampleRate, m_incoming_spec.chanCount, m_incoming_spec.chanMask));
|
|
|
133 m_incoming_ptr += delta;
|
|
|
134 if (m_eos && this->queue_empty()) {
|
|
|
135 this->send_force_play();
|
|
|
136 }
|
|
|
137 }
|
|
|
138
|
|
|
139 m_can_write -= delta / m_incoming_spec.chanCount;
|
|
|
140 }
|
|
|
141 return m_can_write;
|
|
|
142 }
|
|
|
143
|
|
|
144 double output_impl::get_latency() {
|
|
|
145 double ret = 0;
|
|
|
146 if (m_incoming_spec.is_valid()) {
|
|
|
147 ret += audio_math::samples_to_time( (m_incoming.get_size() - m_incoming_ptr) / m_incoming_spec.chanCount, m_incoming_spec.sampleRate );
|
|
|
148 }
|
|
|
149 if (m_active_spec.is_valid()) {
|
|
|
150 ret += audio_math::samples_to_time( get_latency_samples() , m_active_spec.sampleRate );
|
|
|
151 }
|
|
|
152 return ret;
|
|
|
153 }
|
|
|
154
|
|
|
155 void output_impl::force_play() {
|
|
|
156 if ( m_eos ) return;
|
|
|
157 m_eos = true;
|
|
|
158 if (queue_empty()) send_force_play();
|
|
|
159 }
|
|
|
160 void output_impl::send_force_play() {
|
|
|
161 if (m_sent_force_play) return;
|
|
|
162 m_sent_force_play = true;
|
|
|
163 this->on_force_play();
|
|
|
164 }
|
|
|
165
|
|
|
166 static void spec_sanity(audio_chunk::spec_t const& spec) {
|
|
|
167 if (!spec.is_valid()) pfc::throw_exception_with_message< exception_io_data >("Invalid audio stream specifications");
|
|
|
168 }
|
|
|
169
|
|
|
170 size_t output_impl::process_samples_v2(const audio_chunk& p_chunk) {
|
|
|
171 PFC_ASSERT(queue_empty());
|
|
|
172 PFC_ASSERT(!m_eos);
|
|
|
173 const auto spec = p_chunk.get_spec();
|
|
|
174 if (m_incoming_spec != spec) {
|
|
|
175 spec_sanity(spec);
|
|
|
176 m_incoming_spec = spec;
|
|
|
177 return 0;
|
|
|
178 }
|
|
|
179
|
|
|
180 auto in = p_chunk.get_sample_count();
|
|
|
181 if (in > m_can_write) in = m_can_write;
|
|
|
182 if (in > 0) {
|
|
|
183 write(audio_chunk_partial_ref(p_chunk, 0, in));
|
|
|
184 m_can_write -= in;
|
|
|
185 }
|
|
|
186 return in;
|
|
|
187 }
|
|
|
188
|
|
|
189 void output_impl::process_samples(const audio_chunk & p_chunk) {
|
|
|
190 PFC_ASSERT(queue_empty());
|
|
|
191 PFC_ASSERT( !m_eos );
|
|
|
192 const auto spec = p_chunk.get_spec();
|
|
|
193 size_t taken = 0;
|
|
|
194 if (m_incoming_spec == spec) {
|
|
|
195 // Try bypassing intermediate buffer
|
|
|
196 taken = this->process_samples_v2(p_chunk);
|
|
|
197 if (taken == p_chunk.get_sample_count()) return; // all written, success
|
|
|
198 taken *= spec.chanCount;
|
|
|
199 } else {
|
|
|
200 spec_sanity(spec);
|
|
|
201 m_incoming_spec = spec;
|
|
|
202 }
|
|
|
203 // Queue what's left for update() to eat later
|
|
|
204 m_incoming.set_data_fromptr(p_chunk.get_data() + taken, p_chunk.get_used_size() - taken);
|
|
|
205 m_incoming_ptr = 0;
|
|
|
206 }
|
|
|
207
|
|
|
208 void output_v3::get_injected_dsps( dsp_chain_config & dsps ) {
|
|
|
209 dsps.remove_all();
|
|
|
210 }
|
|
|
211
|
|
|
212 size_t output_v4::update_v2() {
|
|
|
213 bool bReady = false;
|
|
|
214 update(bReady);
|
|
|
215 return bReady ? SIZE_MAX : 0;
|
|
|
216 }
|
|
|
217
|
|
|
218 uint32_t output_entry::get_config_flags_compat() {
|
|
|
219 uint32_t ret = get_config_flags();
|
|
|
220 if ((ret & (flag_low_latency | flag_high_latency)) == 0) {
|
|
|
221 // output predating flag_high_latency + flag_low_latency
|
|
|
222 // if it's old foo_out_upnp, report high latency, otherwise low latency.
|
|
|
223 static const GUID guid_foo_out_upnp = { 0x9900b4f6, 0x8431, 0x4b0a, { 0x95, 0x56, 0xa7, 0xfc, 0xb9, 0x5b, 0x74, 0x3 } };
|
|
|
224 if (this->get_guid() == guid_foo_out_upnp) ret |= flag_high_latency;
|
|
|
225 else ret |= flag_low_latency;
|
|
|
226 }
|
|
|
227 return ret;
|
|
|
228 }
|
|
|
229
|
|
|
230 bool output_entry::is_high_latency() {
|
|
|
231 return (this->get_config_flags_compat() & flag_high_latency) != 0;
|
|
|
232 }
|
|
|
233
|
|
|
234 bool output_entry::is_low_latency() {
|
|
|
235 return (this->get_config_flags_compat() & flag_low_latency) != 0;
|
|
|
236 }
|
|
|
237
|
|
|
238 // {EEEB07DE-C2C8-44c2-985C-C85856D96DA1}
|
|
|
239 const GUID output_id_null =
|
|
|
240 { 0xeeeb07de, 0xc2c8, 0x44c2, { 0x98, 0x5c, 0xc8, 0x58, 0x56, 0xd9, 0x6d, 0xa1 } };
|
|
|
241
|
|
|
242 // {D41D2423-FBB0-4635-B233-7054F79814AB}
|
|
|
243 const GUID output_id_default =
|
|
|
244 { 0xd41d2423, 0xfbb0, 0x4635, { 0xb2, 0x33, 0x70, 0x54, 0xf7, 0x98, 0x14, 0xab } };
|
|
|
245
|
|
|
246 outputCoreConfig_t outputCoreConfig_t::defaults() {
|
|
|
247 outputCoreConfig_t cfg = {};
|
|
|
248 cfg.m_bitDepth = 16;
|
|
|
249 cfg.m_buffer_length = 1.0;
|
|
|
250 cfg.m_output = output_id_default;
|
|
|
251 // remaining fields nulled by {}
|
|
|
252 return cfg;
|
|
|
253 }
|
|
|
254 namespace {
|
|
|
255 class output_device_list_callback_impl : public output_device_list_callback {
|
|
|
256 public:
|
|
|
257 void onDevice( const char * fullName, const GUID & output, const GUID & device ) {
|
|
|
258 f(fullName, output, device);
|
|
|
259 }
|
|
|
260 std::function< void ( const char*, const GUID&, const GUID&) > f;
|
|
|
261 };
|
|
|
262
|
|
|
263 class output_config_change_callback_impl : public output_config_change_callback {
|
|
|
264 public:
|
|
|
265 void outputConfigChanged() {
|
|
|
266 f();
|
|
|
267 }
|
|
|
268 std::function<void () > f;
|
|
|
269 };
|
|
|
270 }
|
|
|
271 void output_manager_v2::listDevices( std::function< void ( const char*, const GUID&, const GUID&) > f ) {
|
|
|
272 output_device_list_callback_impl cb; cb.f = f;
|
|
|
273 this->listDevices( cb );
|
|
|
274 }
|
|
|
275
|
|
|
276 service_ptr output_manager_v2::addCallback( std::function<void() > f ) {
|
|
|
277 output_config_change_callback_impl * obj = new output_config_change_callback_impl();
|
|
|
278 obj->f = f;
|
|
|
279 this->addCallback( obj );
|
|
|
280 service_ptr_t<output_manager_v2> selfRef ( this );
|
|
|
281 return fb2k::callOnRelease( [obj, selfRef] {
|
|
|
282 selfRef->removeCallback( obj ); delete obj;
|
|
|
283 } );
|
|
|
284 }
|
|
|
285
|
|
|
286 void output_manager_v2::addCallbackPermanent( std::function<void()> f ) {
|
|
|
287 output_config_change_callback_impl * obj = new output_config_change_callback_impl();
|
|
|
288 obj->f = f;
|
|
|
289 addCallback( obj );
|
|
|
290 }
|
|
|
291
|
|
|
292 void output_manager::getCoreConfig(outputCoreConfig_t& out) {
|
|
|
293 getCoreConfig(&out, sizeof(out));
|
|
|
294 }
|
|
|
295
|
|
|
296 outputCoreConfig_t output_manager::getCoreConfig() {
|
|
|
297 outputCoreConfig_t ret; getCoreConfig(ret); return ret;
|
|
|
298 }
|