/src/mozilla-central/dom/media/AudioStream.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
2 | | /* vim:set ts=2 sw=2 sts=2 et cindent: */ |
3 | | /* This Source Code Form is subject to the terms of the Mozilla Public |
4 | | * License, v. 2.0. If a copy of the MPL was not distributed with this |
5 | | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
6 | | #include <stdio.h> |
7 | | #include <math.h> |
8 | | #include <string.h> |
9 | | #include "mozilla/Logging.h" |
10 | | #include "prdtoa.h" |
11 | | #include "AudioStream.h" |
12 | | #include "VideoUtils.h" |
13 | | #include "mozilla/Monitor.h" |
14 | | #include "mozilla/Mutex.h" |
15 | | #include "mozilla/Sprintf.h" |
16 | | #include "mozilla/Unused.h" |
17 | | #include <algorithm> |
18 | | #include "mozilla/Telemetry.h" |
19 | | #include "CubebUtils.h" |
20 | | #include "nsPrintfCString.h" |
21 | | #include "gfxPrefs.h" |
22 | | #include "AudioConverter.h" |
23 | | #if defined(XP_WIN) |
24 | | #include "nsXULAppAPI.h" |
25 | | #endif |
26 | | |
27 | | namespace mozilla { |
28 | | |
29 | | #undef LOG |
30 | | #undef LOGW |
31 | | #undef LOGE |
32 | | |
33 | | LazyLogModule gAudioStreamLog("AudioStream"); |
34 | | // For simple logs |
35 | 0 | #define LOG(x, ...) MOZ_LOG(gAudioStreamLog, mozilla::LogLevel::Debug, ("%p " x, this, ##__VA_ARGS__)) |
36 | 0 | #define LOGW(x, ...) MOZ_LOG(gAudioStreamLog, mozilla::LogLevel::Warning, ("%p " x, this, ##__VA_ARGS__)) |
37 | 0 | #define LOGE(x, ...) NS_DebugBreak(NS_DEBUG_WARNING, nsPrintfCString("%p " x, this, ##__VA_ARGS__).get(), nullptr, __FILE__, __LINE__) |
38 | | |
39 | | /** |
40 | | * Keep a list of frames sent to the audio engine in each DataCallback along |
41 | | * with the playback rate at the moment. Since the playback rate and number of |
42 | | * underrun frames can vary in each callback. We need to keep the whole history |
43 | | * in order to calculate the playback position of the audio engine correctly. |
44 | | */ |
45 | | class FrameHistory { |
46 | | struct Chunk { |
47 | | uint32_t servicedFrames; |
48 | | uint32_t totalFrames; |
49 | | uint32_t rate; |
50 | | }; |
51 | | |
52 | | template <typename T> |
53 | 0 | static T FramesToUs(uint32_t frames, int rate) { |
54 | 0 | return static_cast<T>(frames) * USECS_PER_S / rate; |
55 | 0 | } Unexecuted instantiation: long mozilla::FrameHistory::FramesToUs<long>(unsigned int, int) Unexecuted instantiation: double mozilla::FrameHistory::FramesToUs<double>(unsigned int, int) |
56 | | public: |
57 | | FrameHistory() |
58 | 0 | : mBaseOffset(0), mBasePosition(0) {} |
59 | | |
60 | 0 | void Append(uint32_t aServiced, uint32_t aUnderrun, uint32_t aRate) { |
61 | 0 | /* In most case where playback rate stays the same and we don't underrun |
62 | 0 | * frames, we are able to merge chunks to avoid lose of precision to add up |
63 | 0 | * in compressing chunks into |mBaseOffset| and |mBasePosition|. |
64 | 0 | */ |
65 | 0 | if (!mChunks.IsEmpty()) { |
66 | 0 | Chunk& c = mChunks.LastElement(); |
67 | 0 | // 2 chunks (c1 and c2) can be merged when rate is the same and |
68 | 0 | // adjacent frames are zero. That is, underrun frames in c1 are zero |
69 | 0 | // or serviced frames in c2 are zero. |
70 | 0 | if (c.rate == aRate && |
71 | 0 | (c.servicedFrames == c.totalFrames || |
72 | 0 | aServiced == 0)) { |
73 | 0 | c.servicedFrames += aServiced; |
74 | 0 | c.totalFrames += aServiced + aUnderrun; |
75 | 0 | return; |
76 | 0 | } |
77 | 0 | } |
78 | 0 | Chunk* p = mChunks.AppendElement(); |
79 | 0 | p->servicedFrames = aServiced; |
80 | 0 | p->totalFrames = aServiced + aUnderrun; |
81 | 0 | p->rate = aRate; |
82 | 0 | } |
83 | | |
84 | | /** |
85 | | * @param frames The playback position in frames of the audio engine. |
86 | | * @return The playback position in microseconds of the audio engine, |
87 | | * adjusted by playback rate changes and underrun frames. |
88 | | */ |
89 | 0 | int64_t GetPosition(int64_t frames) { |
90 | 0 | // playback position should not go backward. |
91 | 0 | MOZ_ASSERT(frames >= mBaseOffset); |
92 | 0 | while (true) { |
93 | 0 | if (mChunks.IsEmpty()) { |
94 | 0 | return mBasePosition; |
95 | 0 | } |
96 | 0 | const Chunk& c = mChunks[0]; |
97 | 0 | if (frames <= mBaseOffset + c.totalFrames) { |
98 | 0 | uint32_t delta = frames - mBaseOffset; |
99 | 0 | delta = std::min(delta, c.servicedFrames); |
100 | 0 | return static_cast<int64_t>(mBasePosition) + |
101 | 0 | FramesToUs<int64_t>(delta, c.rate); |
102 | 0 | } |
103 | 0 | // Since the playback position of the audio engine will not go backward, |
104 | 0 | // we are able to compress chunks so that |mChunks| won't grow unlimitedly. |
105 | 0 | // Note that we lose precision in converting integers into floats and |
106 | 0 | // inaccuracy will accumulate over time. However, for a 24hr long, |
107 | 0 | // sample rate = 44.1k file, the error will be less than 1 microsecond |
108 | 0 | // after playing 24 hours. So we are fine with that. |
109 | 0 | mBaseOffset += c.totalFrames; |
110 | 0 | mBasePosition += FramesToUs<double>(c.servicedFrames, c.rate); |
111 | 0 | mChunks.RemoveElementAt(0); |
112 | 0 | } |
113 | 0 | } |
114 | | private: |
115 | | AutoTArray<Chunk, 7> mChunks; |
116 | | int64_t mBaseOffset; |
117 | | double mBasePosition; |
118 | | }; |
119 | | |
120 | | AudioStream::AudioStream(DataSource& aSource) |
121 | | : mMonitor("AudioStream") |
122 | | , mChannels(0) |
123 | | , mOutChannels(0) |
124 | | , mTimeStretcher(nullptr) |
125 | | , mDumpFile(nullptr) |
126 | | , mState(INITIALIZED) |
127 | | , mDataSource(aSource) |
128 | | , mPrefillQuirk(false) |
129 | 0 | { |
130 | | #if defined(XP_WIN) |
131 | | if (XRE_IsContentProcess()) { |
132 | | audio::AudioNotificationReceiver::Register(this); |
133 | | } |
134 | | #endif |
135 | | } |
136 | | |
137 | | AudioStream::~AudioStream() |
138 | 0 | { |
139 | 0 | LOG("deleted, state %d", mState); |
140 | 0 | MOZ_ASSERT(mState == SHUTDOWN && !mCubebStream, |
141 | 0 | "Should've called Shutdown() before deleting an AudioStream"); |
142 | 0 | if (mDumpFile) { |
143 | 0 | fclose(mDumpFile); |
144 | 0 | } |
145 | 0 | if (mTimeStretcher) { |
146 | 0 | soundtouch::destroySoundTouchObj(mTimeStretcher); |
147 | 0 | } |
148 | | #if defined(XP_WIN) |
149 | | if (XRE_IsContentProcess()) { |
150 | | audio::AudioNotificationReceiver::Unregister(this); |
151 | | } |
152 | | #endif |
153 | | } |
154 | | |
155 | | size_t |
156 | | AudioStream::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const |
157 | 0 | { |
158 | 0 | size_t amount = aMallocSizeOf(this); |
159 | 0 |
|
160 | 0 | // Possibly add in the future: |
161 | 0 | // - mTimeStretcher |
162 | 0 | // - mCubebStream |
163 | 0 |
|
164 | 0 | return amount; |
165 | 0 | } |
166 | | |
167 | | nsresult AudioStream::EnsureTimeStretcherInitializedUnlocked() |
168 | 0 | { |
169 | 0 | mMonitor.AssertCurrentThreadOwns(); |
170 | 0 | if (!mTimeStretcher) { |
171 | 0 | mTimeStretcher = soundtouch::createSoundTouchObj(); |
172 | 0 | mTimeStretcher->setSampleRate(mAudioClock.GetInputRate()); |
173 | 0 | mTimeStretcher->setChannels(mOutChannels); |
174 | 0 | mTimeStretcher->setPitch(1.0); |
175 | 0 | } |
176 | 0 | return NS_OK; |
177 | 0 | } |
178 | | |
179 | | nsresult AudioStream::SetPlaybackRate(double aPlaybackRate) |
180 | 0 | { |
181 | 0 | // MUST lock since the rate transposer is used from the cubeb callback, |
182 | 0 | // and rate changes can cause the buffer to be reallocated |
183 | 0 | MonitorAutoLock mon(mMonitor); |
184 | 0 |
|
185 | 0 | NS_ASSERTION(aPlaybackRate > 0.0, |
186 | 0 | "Can't handle negative or null playbackrate in the AudioStream."); |
187 | 0 | // Avoid instantiating the resampler if we are not changing the playback rate. |
188 | 0 | // GetPreservesPitch/SetPreservesPitch don't need locking before calling |
189 | 0 | if (aPlaybackRate == mAudioClock.GetPlaybackRate()) { |
190 | 0 | return NS_OK; |
191 | 0 | } |
192 | 0 | |
193 | 0 | if (EnsureTimeStretcherInitializedUnlocked() != NS_OK) { |
194 | 0 | return NS_ERROR_FAILURE; |
195 | 0 | } |
196 | 0 | |
197 | 0 | mAudioClock.SetPlaybackRate(aPlaybackRate); |
198 | 0 |
|
199 | 0 | if (mAudioClock.GetPreservesPitch()) { |
200 | 0 | mTimeStretcher->setTempo(aPlaybackRate); |
201 | 0 | mTimeStretcher->setRate(1.0f); |
202 | 0 | } else { |
203 | 0 | mTimeStretcher->setTempo(1.0f); |
204 | 0 | mTimeStretcher->setRate(aPlaybackRate); |
205 | 0 | } |
206 | 0 | return NS_OK; |
207 | 0 | } |
208 | | |
209 | | nsresult AudioStream::SetPreservesPitch(bool aPreservesPitch) |
210 | 0 | { |
211 | 0 | // MUST lock since the rate transposer is used from the cubeb callback, |
212 | 0 | // and rate changes can cause the buffer to be reallocated |
213 | 0 | MonitorAutoLock mon(mMonitor); |
214 | 0 |
|
215 | 0 | // Avoid instantiating the timestretcher instance if not needed. |
216 | 0 | if (aPreservesPitch == mAudioClock.GetPreservesPitch()) { |
217 | 0 | return NS_OK; |
218 | 0 | } |
219 | 0 | |
220 | 0 | if (EnsureTimeStretcherInitializedUnlocked() != NS_OK) { |
221 | 0 | return NS_ERROR_FAILURE; |
222 | 0 | } |
223 | 0 | |
224 | 0 | if (aPreservesPitch == true) { |
225 | 0 | mTimeStretcher->setTempo(mAudioClock.GetPlaybackRate()); |
226 | 0 | mTimeStretcher->setRate(1.0f); |
227 | 0 | } else { |
228 | 0 | mTimeStretcher->setTempo(1.0f); |
229 | 0 | mTimeStretcher->setRate(mAudioClock.GetPlaybackRate()); |
230 | 0 | } |
231 | 0 |
|
232 | 0 | mAudioClock.SetPreservesPitch(aPreservesPitch); |
233 | 0 |
|
234 | 0 | return NS_OK; |
235 | 0 | } |
236 | | |
237 | | static void SetUint16LE(uint8_t* aDest, uint16_t aValue) |
238 | 0 | { |
239 | 0 | aDest[0] = aValue & 0xFF; |
240 | 0 | aDest[1] = aValue >> 8; |
241 | 0 | } |
242 | | |
243 | | static void SetUint32LE(uint8_t* aDest, uint32_t aValue) |
244 | 0 | { |
245 | 0 | SetUint16LE(aDest, aValue & 0xFFFF); |
246 | 0 | SetUint16LE(aDest + 2, aValue >> 16); |
247 | 0 | } |
248 | | |
249 | | static FILE* |
250 | | OpenDumpFile(uint32_t aChannels, uint32_t aRate) |
251 | 0 | { |
252 | 0 | /** |
253 | 0 | * When MOZ_DUMP_AUDIO is set in the environment (to anything), |
254 | 0 | * we'll drop a series of files in the current working directory named |
255 | 0 | * dumped-audio-<nnn>.wav, one per AudioStream created, containing |
256 | 0 | * the audio for the stream including any skips due to underruns. |
257 | 0 | */ |
258 | 0 | static Atomic<int> gDumpedAudioCount(0); |
259 | 0 |
|
260 | 0 | if (!getenv("MOZ_DUMP_AUDIO")) |
261 | 0 | return nullptr; |
262 | 0 | char buf[100]; |
263 | 0 | SprintfLiteral(buf, "dumped-audio-%d.wav", ++gDumpedAudioCount); |
264 | 0 | FILE* f = fopen(buf, "wb"); |
265 | 0 | if (!f) |
266 | 0 | return nullptr; |
267 | 0 | |
268 | 0 | uint8_t header[] = { |
269 | 0 | // RIFF header |
270 | 0 | 0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x41, 0x56, 0x45, |
271 | 0 | // fmt chunk. We always write 16-bit samples. |
272 | 0 | 0x66, 0x6d, 0x74, 0x20, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0xFF, 0xFF, |
273 | 0 | 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x10, 0x00, |
274 | 0 | // data chunk |
275 | 0 | 0x64, 0x61, 0x74, 0x61, 0xFE, 0xFF, 0xFF, 0x7F |
276 | 0 | }; |
277 | 0 | static const int CHANNEL_OFFSET = 22; |
278 | 0 | static const int SAMPLE_RATE_OFFSET = 24; |
279 | 0 | static const int BLOCK_ALIGN_OFFSET = 32; |
280 | 0 | SetUint16LE(header + CHANNEL_OFFSET, aChannels); |
281 | 0 | SetUint32LE(header + SAMPLE_RATE_OFFSET, aRate); |
282 | 0 | SetUint16LE(header + BLOCK_ALIGN_OFFSET, aChannels * 2); |
283 | 0 | Unused << fwrite(header, sizeof(header), 1, f); |
284 | 0 |
|
285 | 0 | return f; |
286 | 0 | } |
287 | | |
288 | | template <typename T> |
289 | | typename EnableIf<IsSame<T, int16_t>::value, void>::Type |
290 | | WriteDumpFileHelper(T* aInput, size_t aSamples, FILE* aFile) { |
291 | | Unused << fwrite(aInput, sizeof(T), aSamples, aFile); |
292 | | } |
293 | | |
294 | | template <typename T> |
295 | | typename EnableIf<IsSame<T, float>::value, void>::Type |
296 | 0 | WriteDumpFileHelper(T* aInput, size_t aSamples, FILE* aFile) { |
297 | 0 | AutoTArray<uint8_t, 1024*2> buf; |
298 | 0 | buf.SetLength(aSamples*2); |
299 | 0 | uint8_t* output = buf.Elements(); |
300 | 0 | for (uint32_t i = 0; i < aSamples; ++i) { |
301 | 0 | SetUint16LE(output + i*2, int16_t(aInput[i]*32767.0f)); |
302 | 0 | } |
303 | 0 | Unused << fwrite(output, 2, aSamples, aFile); |
304 | 0 | fflush(aFile); |
305 | 0 | } |
306 | | |
307 | | static void |
308 | | WriteDumpFile(FILE* aDumpFile, AudioStream* aStream, uint32_t aFrames, |
309 | | void* aBuffer) |
310 | 0 | { |
311 | 0 | if (!aDumpFile) |
312 | 0 | return; |
313 | 0 | |
314 | 0 | uint32_t samples = aStream->GetOutChannels()*aFrames; |
315 | 0 |
|
316 | 0 | using SampleT = AudioSampleTraits<AUDIO_OUTPUT_FORMAT>::Type; |
317 | 0 | WriteDumpFileHelper(reinterpret_cast<SampleT*>(aBuffer), samples, aDumpFile); |
318 | 0 | } |
319 | | |
320 | | template <AudioSampleFormat N> |
321 | | struct ToCubebFormat { |
322 | | static const cubeb_sample_format value = CUBEB_SAMPLE_FLOAT32NE; |
323 | | }; |
324 | | |
325 | | template <> |
326 | | struct ToCubebFormat<AUDIO_FORMAT_S16> { |
327 | | static const cubeb_sample_format value = CUBEB_SAMPLE_S16NE; |
328 | | }; |
329 | | |
330 | | template <typename Function, typename... Args> |
331 | | int AudioStream::InvokeCubeb(Function aFunction, Args&&... aArgs) |
332 | 0 | { |
333 | 0 | MonitorAutoUnlock mon(mMonitor); |
334 | 0 | return aFunction(mCubebStream.get(), std::forward<Args>(aArgs)...); |
335 | 0 | } Unexecuted instantiation: int mozilla::AudioStream::InvokeCubeb<int (*)(cubeb_stream*)>(int (*)(cubeb_stream*)) Unexecuted instantiation: int mozilla::AudioStream::InvokeCubeb<int (*)(cubeb_stream*, unsigned long*), unsigned long*>(int (*)(cubeb_stream*, unsigned long*), unsigned long*&&) |
336 | | |
337 | | nsresult |
338 | | AudioStream::Init(uint32_t aNumChannels, |
339 | | AudioConfig::ChannelLayout::ChannelMap aChannelMap, |
340 | | uint32_t aRate) |
341 | 0 | { |
342 | 0 | auto startTime = TimeStamp::Now(); |
343 | 0 |
|
344 | 0 | LOG("%s channels: %d, rate: %d", __FUNCTION__, aNumChannels, aRate); |
345 | 0 | mChannels = aNumChannels; |
346 | 0 | mOutChannels = aNumChannels; |
347 | 0 |
|
348 | 0 | mDumpFile = OpenDumpFile(aNumChannels, aRate); |
349 | 0 |
|
350 | 0 | cubeb_stream_params params; |
351 | 0 | params.rate = aRate; |
352 | 0 | params.channels = mOutChannels; |
353 | 0 | params.layout = static_cast<uint32_t>(aChannelMap); |
354 | 0 | params.format = ToCubebFormat<AUDIO_OUTPUT_FORMAT>::value; |
355 | 0 | params.prefs = CubebUtils::GetDefaultStreamPrefs(); |
356 | 0 |
|
357 | 0 | mAudioClock.Init(aRate); |
358 | 0 |
|
359 | 0 | cubeb* cubebContext = CubebUtils::GetCubebContext(); |
360 | 0 | if (!cubebContext) { |
361 | 0 | LOGE("Can't get cubeb context!"); |
362 | 0 | CubebUtils::ReportCubebStreamInitFailure(true); |
363 | 0 | return NS_ERROR_DOM_MEDIA_CUBEB_INITIALIZATION_ERR; |
364 | 0 | } |
365 | 0 |
|
366 | 0 | // cubeb's winmm backend prefills buffers on init rather than stream start. |
367 | 0 | // See https://github.com/kinetiknz/cubeb/issues/150 |
368 | 0 | mPrefillQuirk = !strcmp(cubeb_get_backend_id(cubebContext), "winmm"); |
369 | 0 |
|
370 | 0 | return OpenCubeb(cubebContext, params, startTime, CubebUtils::GetFirstStream()); |
371 | 0 | } |
372 | | |
373 | | nsresult |
374 | | AudioStream::OpenCubeb(cubeb* aContext, cubeb_stream_params& aParams, |
375 | | TimeStamp aStartTime, bool aIsFirst) |
376 | 0 | { |
377 | 0 | MOZ_ASSERT(aContext); |
378 | 0 |
|
379 | 0 | cubeb_stream* stream = nullptr; |
380 | 0 | /* Convert from milliseconds to frames. */ |
381 | 0 | uint32_t latency_frames = |
382 | 0 | CubebUtils::GetCubebPlaybackLatencyInMilliseconds() * aParams.rate / 1000; |
383 | 0 | if (cubeb_stream_init(aContext, &stream, "AudioStream", |
384 | 0 | nullptr, nullptr, nullptr, &aParams, |
385 | 0 | latency_frames, |
386 | 0 | DataCallback_S, StateCallback_S, this) == CUBEB_OK) { |
387 | 0 | mCubebStream.reset(stream); |
388 | 0 | CubebUtils::ReportCubebBackendUsed(); |
389 | 0 | } else { |
390 | 0 | LOGE("OpenCubeb() failed to init cubeb"); |
391 | 0 | CubebUtils::ReportCubebStreamInitFailure(aIsFirst); |
392 | 0 | return NS_ERROR_FAILURE; |
393 | 0 | } |
394 | 0 |
|
395 | 0 | TimeDuration timeDelta = TimeStamp::Now() - aStartTime; |
396 | 0 | LOG("creation time %sfirst: %u ms", aIsFirst ? "" : "not ", |
397 | 0 | (uint32_t) timeDelta.ToMilliseconds()); |
398 | 0 | Telemetry::Accumulate(aIsFirst ? Telemetry::AUDIOSTREAM_FIRST_OPEN_MS : |
399 | 0 | Telemetry::AUDIOSTREAM_LATER_OPEN_MS, timeDelta.ToMilliseconds()); |
400 | 0 |
|
401 | 0 | return NS_OK; |
402 | 0 | } |
403 | | |
404 | | void |
405 | | AudioStream::SetVolume(double aVolume) |
406 | 0 | { |
407 | 0 | MOZ_ASSERT(aVolume >= 0.0 && aVolume <= 1.0, "Invalid volume"); |
408 | 0 |
|
409 | 0 | if (cubeb_stream_set_volume(mCubebStream.get(), aVolume * CubebUtils::GetVolumeScale()) != CUBEB_OK) { |
410 | 0 | LOGE("Could not change volume on cubeb stream."); |
411 | 0 | } |
412 | 0 | } |
413 | | |
414 | | void |
415 | | AudioStream::Start() |
416 | 0 | { |
417 | 0 | MonitorAutoLock mon(mMonitor); |
418 | 0 | MOZ_ASSERT(mState == INITIALIZED); |
419 | 0 | mState = STARTED; |
420 | 0 | auto r = InvokeCubeb(cubeb_stream_start); |
421 | 0 | if (r != CUBEB_OK) { |
422 | 0 | mState = ERRORED; |
423 | 0 | } |
424 | 0 | LOG("started, state %s", mState == STARTED ? "STARTED" : mState == DRAINED ? "DRAINED" : "ERRORED"); |
425 | 0 | } |
426 | | |
427 | | void |
428 | | AudioStream::Pause() |
429 | 0 | { |
430 | 0 | MonitorAutoLock mon(mMonitor); |
431 | 0 | MOZ_ASSERT(mState != INITIALIZED, "Must be Start()ed."); |
432 | 0 | MOZ_ASSERT(mState != STOPPED, "Already Pause()ed."); |
433 | 0 | MOZ_ASSERT(mState != SHUTDOWN, "Already Shutdown()ed."); |
434 | 0 |
|
435 | 0 | // Do nothing if we are already drained or errored. |
436 | 0 | if (mState == DRAINED || mState == ERRORED) { |
437 | 0 | return; |
438 | 0 | } |
439 | 0 | |
440 | 0 | if (InvokeCubeb(cubeb_stream_stop) != CUBEB_OK) { |
441 | 0 | mState = ERRORED; |
442 | 0 | } else if (mState != DRAINED && mState != ERRORED) { |
443 | 0 | // Don't transition to other states if we are already |
444 | 0 | // drained or errored. |
445 | 0 | mState = STOPPED; |
446 | 0 | } |
447 | 0 | } |
448 | | |
449 | | void |
450 | | AudioStream::Resume() |
451 | 0 | { |
452 | 0 | MonitorAutoLock mon(mMonitor); |
453 | 0 | MOZ_ASSERT(mState != INITIALIZED, "Must be Start()ed."); |
454 | 0 | MOZ_ASSERT(mState != STARTED, "Already Start()ed."); |
455 | 0 | MOZ_ASSERT(mState != SHUTDOWN, "Already Shutdown()ed."); |
456 | 0 |
|
457 | 0 | // Do nothing if we are already drained or errored. |
458 | 0 | if (mState == DRAINED || mState == ERRORED) { |
459 | 0 | return; |
460 | 0 | } |
461 | 0 | |
462 | 0 | if (InvokeCubeb(cubeb_stream_start) != CUBEB_OK) { |
463 | 0 | mState = ERRORED; |
464 | 0 | } else if (mState != DRAINED && mState != ERRORED) { |
465 | 0 | // Don't transition to other states if we are already |
466 | 0 | // drained or errored. |
467 | 0 | mState = STARTED; |
468 | 0 | } |
469 | 0 | } |
470 | | |
471 | | void |
472 | | AudioStream::Shutdown() |
473 | 0 | { |
474 | 0 | MonitorAutoLock mon(mMonitor); |
475 | 0 | LOG("Shutdown, state %d", mState); |
476 | 0 |
|
477 | 0 | if (mCubebStream) { |
478 | 0 | MonitorAutoUnlock mon(mMonitor); |
479 | 0 | // Force stop to put the cubeb stream in a stable state before deletion. |
480 | 0 | cubeb_stream_stop(mCubebStream.get()); |
481 | 0 | // Must not try to shut down cubeb from within the lock! wasapi may still |
482 | 0 | // call our callback after Pause()/stop()!?! Bug 996162 |
483 | 0 | mCubebStream.reset(); |
484 | 0 | } |
485 | 0 |
|
486 | 0 | mState = SHUTDOWN; |
487 | 0 | } |
488 | | |
489 | | #if defined(XP_WIN) |
490 | | void |
491 | | AudioStream::ResetDefaultDevice() |
492 | | { |
493 | | MonitorAutoLock mon(mMonitor); |
494 | | if (mState != STARTED && mState != STOPPED) { |
495 | | return; |
496 | | } |
497 | | |
498 | | MOZ_ASSERT(mCubebStream); |
499 | | auto r = InvokeCubeb(cubeb_stream_reset_default_device); |
500 | | if (!(r == CUBEB_OK || r == CUBEB_ERROR_NOT_SUPPORTED)) { |
501 | | mState = ERRORED; |
502 | | } |
503 | | } |
504 | | #endif |
505 | | |
506 | | int64_t |
507 | | AudioStream::GetPosition() |
508 | 0 | { |
509 | 0 | MonitorAutoLock mon(mMonitor); |
510 | 0 | int64_t frames = GetPositionInFramesUnlocked(); |
511 | 0 | return frames >= 0 ? mAudioClock.GetPosition(frames) : -1; |
512 | 0 | } |
513 | | |
514 | | int64_t |
515 | | AudioStream::GetPositionInFrames() |
516 | 0 | { |
517 | 0 | MonitorAutoLock mon(mMonitor); |
518 | 0 | int64_t frames = GetPositionInFramesUnlocked(); |
519 | 0 | return frames >= 0 ? mAudioClock.GetPositionInFrames(frames) : -1; |
520 | 0 | } |
521 | | |
522 | | int64_t |
523 | | AudioStream::GetPositionInFramesUnlocked() |
524 | 0 | { |
525 | 0 | mMonitor.AssertCurrentThreadOwns(); |
526 | 0 |
|
527 | 0 | if (mState == ERRORED) { |
528 | 0 | return -1; |
529 | 0 | } |
530 | 0 | |
531 | 0 | uint64_t position = 0; |
532 | 0 | if (InvokeCubeb(cubeb_stream_get_position, &position) != CUBEB_OK) { |
533 | 0 | return -1; |
534 | 0 | } |
535 | 0 | return std::min<uint64_t>(position, INT64_MAX); |
536 | 0 | } |
537 | | |
538 | | bool |
539 | | AudioStream::IsValidAudioFormat(Chunk* aChunk) |
540 | 0 | { |
541 | 0 | if (aChunk->Rate() != mAudioClock.GetInputRate()) { |
542 | 0 | LOGW("mismatched sample %u, mInRate=%u", aChunk->Rate(), mAudioClock.GetInputRate()); |
543 | 0 | return false; |
544 | 0 | } |
545 | 0 |
|
546 | 0 | if (aChunk->Channels() > 8) { |
547 | 0 | return false; |
548 | 0 | } |
549 | 0 | |
550 | 0 | return true; |
551 | 0 | } |
552 | | |
553 | | void |
554 | | AudioStream::GetUnprocessed(AudioBufferWriter& aWriter) |
555 | 0 | { |
556 | 0 | mMonitor.AssertCurrentThreadOwns(); |
557 | 0 |
|
558 | 0 | // Flush the timestretcher pipeline, if we were playing using a playback rate |
559 | 0 | // other than 1.0. |
560 | 0 | if (mTimeStretcher && mTimeStretcher->numSamples()) { |
561 | 0 | auto timeStretcher = mTimeStretcher; |
562 | 0 | aWriter.Write([timeStretcher] (AudioDataValue* aPtr, uint32_t aFrames) { |
563 | 0 | return timeStretcher->receiveSamples(aPtr, aFrames); |
564 | 0 | }, aWriter.Available()); |
565 | 0 |
|
566 | 0 | // TODO: There might be still unprocessed samples in the stretcher. |
567 | 0 | // We should either remove or flush them so they won't be in the output |
568 | 0 | // next time we switch a playback rate other than 1.0. |
569 | 0 | NS_WARNING_ASSERTION( |
570 | 0 | mTimeStretcher->numUnprocessedSamples() == 0, "no samples"); |
571 | 0 | } |
572 | 0 |
|
573 | 0 | while (aWriter.Available() > 0) { |
574 | 0 | UniquePtr<Chunk> c = mDataSource.PopFrames(aWriter.Available()); |
575 | 0 | if (c->Frames() == 0) { |
576 | 0 | break; |
577 | 0 | } |
578 | 0 | MOZ_ASSERT(c->Frames() <= aWriter.Available()); |
579 | 0 | if (IsValidAudioFormat(c.get())) { |
580 | 0 | aWriter.Write(c->Data(), c->Frames()); |
581 | 0 | } else { |
582 | 0 | // Write silence if invalid format. |
583 | 0 | aWriter.WriteZeros(c->Frames()); |
584 | 0 | } |
585 | 0 | } |
586 | 0 | } |
587 | | |
588 | | void |
589 | | AudioStream::GetTimeStretched(AudioBufferWriter& aWriter) |
590 | 0 | { |
591 | 0 | mMonitor.AssertCurrentThreadOwns(); |
592 | 0 |
|
593 | 0 | // We need to call the non-locking version, because we already have the lock. |
594 | 0 | if (EnsureTimeStretcherInitializedUnlocked() != NS_OK) { |
595 | 0 | return; |
596 | 0 | } |
597 | 0 | |
598 | 0 | uint32_t toPopFrames = |
599 | 0 | ceil(aWriter.Available() * mAudioClock.GetPlaybackRate()); |
600 | 0 |
|
601 | 0 | while (mTimeStretcher->numSamples() < aWriter.Available()) { |
602 | 0 | UniquePtr<Chunk> c = mDataSource.PopFrames(toPopFrames); |
603 | 0 | if (c->Frames() == 0) { |
604 | 0 | break; |
605 | 0 | } |
606 | 0 | MOZ_ASSERT(c->Frames() <= toPopFrames); |
607 | 0 | if (IsValidAudioFormat(c.get())) { |
608 | 0 | mTimeStretcher->putSamples(c->Data(), c->Frames()); |
609 | 0 | } else { |
610 | 0 | // Write silence if invalid format. |
611 | 0 | AutoTArray<AudioDataValue, 1000> buf; |
612 | 0 | auto size = CheckedUint32(mOutChannels) * c->Frames(); |
613 | 0 | if (!size.isValid()) { |
614 | 0 | // The overflow should not happen in normal case. |
615 | 0 | LOGW("Invalid member data: %d channels, %d frames", mOutChannels, c->Frames()); |
616 | 0 | return; |
617 | 0 | } |
618 | 0 | buf.SetLength(size.value()); |
619 | 0 | size = size * sizeof(AudioDataValue); |
620 | 0 | if (!size.isValid()) { |
621 | 0 | LOGW("The required memory size is too large."); |
622 | 0 | return; |
623 | 0 | } |
624 | 0 | memset(buf.Elements(), 0, size.value()); |
625 | 0 | mTimeStretcher->putSamples(buf.Elements(), c->Frames()); |
626 | 0 | } |
627 | 0 | } |
628 | 0 |
|
629 | 0 | auto timeStretcher = mTimeStretcher; |
630 | 0 | aWriter.Write([timeStretcher] (AudioDataValue* aPtr, uint32_t aFrames) { |
631 | 0 | return timeStretcher->receiveSamples(aPtr, aFrames); |
632 | 0 | }, aWriter.Available()); |
633 | 0 | } |
634 | | |
635 | | long |
636 | | AudioStream::DataCallback(void* aBuffer, long aFrames) |
637 | 0 | { |
638 | 0 | MonitorAutoLock mon(mMonitor); |
639 | 0 | MOZ_ASSERT(mState != SHUTDOWN, "No data callback after shutdown"); |
640 | 0 |
|
641 | 0 | auto writer = AudioBufferWriter( |
642 | 0 | reinterpret_cast<AudioDataValue*>(aBuffer), mOutChannels, aFrames); |
643 | 0 |
|
644 | 0 | if (mPrefillQuirk) { |
645 | 0 | // Don't consume audio data until Start() is called. |
646 | 0 | // Expected only with cubeb winmm backend. |
647 | 0 | if (mState == INITIALIZED) { |
648 | 0 | NS_WARNING("data callback fires before cubeb_stream_start() is called"); |
649 | 0 | mAudioClock.UpdateFrameHistory(0, aFrames); |
650 | 0 | return writer.WriteZeros(aFrames); |
651 | 0 | } |
652 | 0 | } else { |
653 | 0 | MOZ_ASSERT(mState != INITIALIZED); |
654 | 0 | } |
655 | 0 |
|
656 | 0 | // NOTE: wasapi (others?) can call us back *after* stop()/Shutdown() (mState == SHUTDOWN) |
657 | 0 | // Bug 996162 |
658 | 0 |
|
659 | 0 | if (mAudioClock.GetInputRate() == mAudioClock.GetOutputRate()) { |
660 | 0 | GetUnprocessed(writer); |
661 | 0 | } else { |
662 | 0 | GetTimeStretched(writer); |
663 | 0 | } |
664 | 0 |
|
665 | 0 | // Always send audible frames first, and silent frames later. |
666 | 0 | // Otherwise it will break the assumption of FrameHistory. |
667 | 0 | if (!mDataSource.Ended()) { |
668 | 0 | mAudioClock.UpdateFrameHistory(aFrames - writer.Available(), writer.Available()); |
669 | 0 | if (writer.Available() > 0) { |
670 | 0 | LOGW("lost %d frames", writer.Available()); |
671 | 0 | writer.WriteZeros(writer.Available()); |
672 | 0 | } |
673 | 0 | } else { |
674 | 0 | // No more new data in the data source. Don't send silent frames so the |
675 | 0 | // cubeb stream can start draining. |
676 | 0 | mAudioClock.UpdateFrameHistory(aFrames - writer.Available(), 0); |
677 | 0 | } |
678 | 0 |
|
679 | 0 | WriteDumpFile(mDumpFile, this, aFrames, aBuffer); |
680 | 0 |
|
681 | 0 | return aFrames - writer.Available(); |
682 | 0 | } |
683 | | |
684 | | void |
685 | | AudioStream::StateCallback(cubeb_state aState) |
686 | 0 | { |
687 | 0 | MonitorAutoLock mon(mMonitor); |
688 | 0 | MOZ_ASSERT(mState != SHUTDOWN, "No state callback after shutdown"); |
689 | 0 | LOG("StateCallback, mState=%d cubeb_state=%d", mState, aState); |
690 | 0 | if (aState == CUBEB_STATE_DRAINED) { |
691 | 0 | mState = DRAINED; |
692 | 0 | mDataSource.Drained(); |
693 | 0 | } else if (aState == CUBEB_STATE_ERROR) { |
694 | 0 | LOGE("StateCallback() state %d cubeb error", mState); |
695 | 0 | mState = ERRORED; |
696 | 0 | } |
697 | 0 | } |
698 | | |
699 | | AudioClock::AudioClock() |
700 | | : mOutRate(0), |
701 | | mInRate(0), |
702 | | mPreservesPitch(true), |
703 | | mFrameHistory(new FrameHistory()) |
704 | 0 | {} |
705 | | |
706 | | void AudioClock::Init(uint32_t aRate) |
707 | 0 | { |
708 | 0 | mOutRate = aRate; |
709 | 0 | mInRate = aRate; |
710 | 0 | } |
711 | | |
712 | | void AudioClock::UpdateFrameHistory(uint32_t aServiced, uint32_t aUnderrun) |
713 | 0 | { |
714 | 0 | mFrameHistory->Append(aServiced, aUnderrun, mOutRate); |
715 | 0 | } |
716 | | |
717 | | int64_t AudioClock::GetPositionInFrames(int64_t aFrames) const |
718 | 0 | { |
719 | 0 | CheckedInt64 v = UsecsToFrames(GetPosition(aFrames), mInRate); |
720 | 0 | return v.isValid() ? v.value() : -1; |
721 | 0 | } |
722 | | |
723 | | int64_t AudioClock::GetPosition(int64_t frames) const |
724 | 0 | { |
725 | 0 | return mFrameHistory->GetPosition(frames); |
726 | 0 | } |
727 | | |
728 | | void AudioClock::SetPlaybackRate(double aPlaybackRate) |
729 | 0 | { |
730 | 0 | mOutRate = static_cast<uint32_t>(mInRate / aPlaybackRate); |
731 | 0 | } |
732 | | |
733 | | double AudioClock::GetPlaybackRate() const |
734 | 0 | { |
735 | 0 | return static_cast<double>(mInRate) / mOutRate; |
736 | 0 | } |
737 | | |
738 | | void AudioClock::SetPreservesPitch(bool aPreservesPitch) |
739 | 0 | { |
740 | 0 | mPreservesPitch = aPreservesPitch; |
741 | 0 | } |
742 | | |
743 | | bool AudioClock::GetPreservesPitch() const |
744 | 0 | { |
745 | 0 | return mPreservesPitch; |
746 | 0 | } |
747 | | |
748 | | } // namespace mozilla |