diff --git a/aegisub/audio_player_pulse.cpp b/aegisub/audio_player_pulse.cpp index 3414fa9da..d1589890d 100644 --- a/aegisub/audio_player_pulse.cpp +++ b/aegisub/audio_player_pulse.cpp @@ -37,12 +37,12 @@ /////////// // Headers #include +#include #include "audio_player.h" #include "audio_provider.h" #include "utils.h" #include "options.h" #include -#include ////////////// @@ -50,54 +50,47 @@ class PulseAudioPlayer; -////////// -// Thread -class PulseAudioPlayerThread : public wxThread { -private: - wxMutex play_mutex; // held while playing - wxSemaphore play_notify; // posted when a playback operation is set up - wxSemaphore stop_notify; // when set, audio playback should stop asap - wxSemaphore shutdown_notify; // when set, thread should shutdown asap - wxMutex parameter_mutex; - - AudioProvider *provider; // provides sample data! - - pa_simple *s; // stream handle - int paerror; - - volatile double volume; // volume to get audio at - volatile unsigned long start_frame; // first frame of playback - volatile unsigned long cur_frame; // last written frame + 1 - volatile unsigned long end_frame; // last frame to play - - void PlaybackLoop(); - -public: - PulseAudioPlayerThread(AudioProvider *_provider); - ~PulseAudioPlayerThread(); - wxThread::ExitCode Entry(); - - // The following methods are all thread safe - void Play(unsigned long _start_frame, unsigned long _num_frames); // Notify thread to stop any playback and instead play specified range - void SetEndPosition(unsigned long _end_frame); // Notify thread to use new end position - void Stop(); // Notify thread to stop audio playback - void StopWait(); // Notify thread to stop, and wait for it to do it - void Shutdown(); // Notify the thread to stop playback and die - void SetVolume(double new_volume); // Set volume - unsigned long GetStartPosition(); // Get first played back frame number - unsigned long GetEndPosition(); // Get last frame number to be played back - unsigned long GetCurrentPosition(); // Get currently played back frame number - bool IsPlaying(); -}; - ////////////////////// // Pulse Audio player class PulseAudioPlayer : public AudioPlayer { private: float volume; + bool open; + bool is_playing; - PulseAudioPlayerThread *thread; + // Audio data info + unsigned long start_frame; + unsigned long cur_frame; + unsigned long end_frame; + unsigned long bpf; // bytes per frame + + // Used for synchronising with async events + wxSemaphore context_notify; + wxSemaphore context_success; + volatile int context_success_val; + wxSemaphore stream_notify; + wxSemaphore stream_success; + volatile int stream_success_val; + + // PulseAudio data + pa_threaded_mainloop *mainloop; // pulseaudio mainloop handle + pa_context *context; // connection context + volatile pa_context_state_t cstate; + pa_stream *stream; + volatile pa_stream_state_t sstate; + int paerror; + + // Called by PA to notify about contetxt operation completion + static void pa_context_success(pa_context *c, int success, PulseAudioPlayer *thread); + // Called by PA to notify about other context-related stuff + static void pa_context_notify(pa_context *c, PulseAudioPlayer *thread); + // Called by PA when a stream operation completes + static void pa_stream_success(pa_stream *p, int success, PulseAudioPlayer *thread); + // Called by PA to request more data written to stream + static void pa_stream_write(pa_stream *p, size_t length, PulseAudioPlayer *thread); + // Called by PA to notify about other stream-related stuff + static void pa_stream_notify(pa_stream *p, PulseAudioPlayer *thread); public: PulseAudioPlayer(); @@ -134,295 +127,308 @@ public: /////////////// // Constructor -PulseAudioPlayer::PulseAudioPlayer() { +PulseAudioPlayer::PulseAudioPlayer() +{ volume = 1.0f; - thread = NULL; + paerror = 0; + open = false; + is_playing = false; } ////////////// // Destructor -PulseAudioPlayer::~PulseAudioPlayer() { - CloseStream(); +PulseAudioPlayer::~PulseAudioPlayer() +{ + if (open) CloseStream(); } /////////////// // Open stream -void PulseAudioPlayer::OpenStream() { - CloseStream(); +void PulseAudioPlayer::OpenStream() +{ + printf("Opening PulseAudio stream\n"); + if (open) CloseStream(); // Get provider AudioProvider *provider = GetProvider(); - try { - thread = new PulseAudioPlayerThread(provider); - thread->Create(); - thread->Run(); + // Initialise a mainloop + printf("Initialising threaded main loop\n"); + mainloop = pa_threaded_mainloop_new(); + if (!mainloop) { + throw _T("Failed to initialise PulseAudio threaded mainloop object"); } - catch (const char *e) { - wxLogError(_T("Failed to initialise PulseAudio: %s"), wxString(e, wxConvLocal).c_str()); + printf("Starting main loop\n"); + pa_threaded_mainloop_start(mainloop); + + // Create context + printf("Creating context\n"); + context = pa_context_new(pa_threaded_mainloop_get_api(mainloop), "Aegisub"); + if (!context) { + pa_threaded_mainloop_free(mainloop); + throw _T("Failed to create PulseAudio context"); } + pa_context_set_state_callback(context, (pa_context_notify_cb_t)pa_context_notify, this); + + // Connect the context + printf("Connecting context\n"); + pa_context_connect(context, NULL, 0, NULL); + // Wait for connection + while (true) { + context_notify.Wait(); + if (cstate == PA_CONTEXT_READY) { + break; + } else if (cstate == PA_CONTEXT_FAILED) { + // eww + paerror = pa_context_errno(context); + pa_context_unref(context); + pa_threaded_mainloop_stop(mainloop); + pa_threaded_mainloop_free(mainloop); + wxString s(pa_strerror(paerror), wxConvUTF8); + throw s.c_str(); + } + // otherwise loop once more + } + printf("Context connected\n"); + + // Set up stream + bpf = provider->GetChannels() * provider->GetBytesPerSample(); + pa_sample_spec ss; + ss.format = PA_SAMPLE_S16LE; // FIXME + ss.rate = provider->GetSampleRate(); + ss.channels = provider->GetChannels(); + pa_channel_map map; + pa_channel_map_init_auto(&map, ss.channels, PA_CHANNEL_MAP_DEFAULT); + printf("Creating stream\n"); + stream = pa_stream_new(context, "Sound", &ss, &map); + if (!stream) { + // argh! + pa_context_disconnect(context); + pa_context_unref(context); + pa_threaded_mainloop_stop(mainloop); + pa_threaded_mainloop_free(mainloop); + throw _T("PulseAudio could not create stream"); + } + pa_stream_set_state_callback(stream, (pa_stream_notify_cb_t)pa_stream_notify, this); + pa_stream_set_write_callback(stream, (pa_stream_request_cb_t)pa_stream_write, this); + + // Connext stream + printf("Connecting playback stream\n"); + paerror = pa_stream_connect_playback(stream, NULL, NULL, PA_STREAM_INTERPOLATE_TIMING|PA_STREAM_NOT_MONOTONOUS|PA_STREAM_AUTO_TIMING_UPDATE, NULL, NULL); + if (paerror) { + printf("PulseAudio reported error: %s\n", pa_strerror(paerror)); + wxString s(pa_strerror(paerror), wxConvUTF8); + throw s.c_str(); + } + while (true) { + stream_notify.Wait(); + if (sstate == PA_STREAM_READY) { + break; + } else if (sstate == PA_STREAM_FAILED) { + printf("Stream connection failed for some reason\n"); + throw _T("Something went wrong connecting the stream"); + } + } + printf("Connected playback stream, now playing\n\n"); + + // Hopefully this marks success + printf("Finished opening PulseAudio\n\n"); + open = true; } //////////////// // Close stream -void PulseAudioPlayer::CloseStream() { - if (!thread) return; +void PulseAudioPlayer::CloseStream() +{ + if (!open) return; + printf("Closing PuseAudio\n"); - thread->Shutdown(); - thread->Wait(); - thread = 0; + if (is_playing) Stop(); + + // Hope for the best and just do things as quickly as possible + pa_stream_disconnect(stream); + pa_stream_unref(stream); + pa_context_disconnect(context); + pa_context_unref(context); + pa_threaded_mainloop_stop(mainloop); + pa_threaded_mainloop_free(mainloop); + + printf("Closed PulseAudio\n"); + open = false; } //////// // Play -void PulseAudioPlayer::Play(__int64 start,__int64 count) { - // Make sure that it's stopped - thread->Stop(); +void PulseAudioPlayer::Play(__int64 start,__int64 count) +{ + printf("Starting PulseAudio playback\n"); + if (!open) OpenStream(); + if (is_playing) Stop(); - thread->Play(start, count); + start_frame = start; + cur_frame = start; + end_frame = start + count; + printf("start=%lu end=%lu\n", start_frame, end_frame); + + is_playing = true; + + PulseAudioPlayer::pa_stream_write(stream, pa_stream_writable_size(stream), this); + + pa_operation *op = pa_stream_trigger(stream, (pa_stream_success_cb_t)pa_stream_success, this); + stream_success.Wait(); + pa_operation_unref(op); } //////// // Stop -void PulseAudioPlayer::Stop(bool timerToo) { - if (thread) thread->Stop(); +void PulseAudioPlayer::Stop(bool timerToo) +{ + if (!is_playing) return; + printf("Stopping PulseAudio\n"); - if (timerToo && displayTimer) { - displayTimer->Stop(); - } + is_playing = false; + + pa_operation *op = pa_stream_cork(stream, 0, (pa_stream_success_cb_t)pa_stream_success, this); + stream_success.Wait(); + pa_operation_unref(op); + + start_frame = 0; + cur_frame = 0; + end_frame = 0; + + // Flush the stream of data + printf("Draining stream\n"); + op = pa_stream_flush(stream, (pa_stream_success_cb_t)pa_stream_success, this); + stream_success.Wait(); + pa_operation_unref(op); + + // Then disconnect it + /*printf("Disconnecting stream\n"); + paerror = pa_stream_disconnect(stream); + if (paerror) { + printf("PulseAudio reported error: %s\n", pa_strerror(paerror)); + wxString s(pa_strerror(paerror), wxConvUTF8); + throw s.c_str(); + } + while (true) { + stream_notify.Wait(); + if (sstate == PA_STREAM_TERMINATED) { + break; + } else if (sstate == PA_STREAM_FAILED) { + printf("Stream stopping failed\n"); + throw _T("Something went wrong disconnecting the stream?!"); + } + }*/ + + // And unref it + printf("Stopped stream\n\n"); } bool PulseAudioPlayer::IsPlaying() { - return thread && thread->IsPlaying(); + return is_playing; } /////////// // Set end -void PulseAudioPlayer::SetEndPosition(__int64 pos) { - if (thread) thread->SetEndPosition(pos); +void PulseAudioPlayer::SetEndPosition(__int64 pos) +{ + end_frame = pos; } //////////////////////// // Set current position -void PulseAudioPlayer::SetCurrentPosition(__int64 pos) { - assert(false); // not supported (yet?) - // I believe this isn't used anywhere. I hope not. +void PulseAudioPlayer::SetCurrentPosition(__int64 pos) +{ + cur_frame = pos; } __int64 PulseAudioPlayer::GetStartPosition() { - if (thread) return thread->GetStartPosition(); - return 0; + return start_frame; } __int64 PulseAudioPlayer::GetEndPosition() { - if (thread) return thread->GetEndPosition(); - return 0; + return end_frame; } //////////////////////// // Get current position -__int64 PulseAudioPlayer::GetCurrentPosition() { - if (thread) return thread->GetCurrentPosition(); - return 0; -} - - - -////////////////////// -// Thread constructor -PulseAudioPlayerThread::PulseAudioPlayerThread(AudioProvider *_provider) -: wxThread(wxTHREAD_JOINABLE) -, play_notify(0, 1) -, stop_notify(0, 1) -, shutdown_notify(0, 1) -, provider(_provider) +__int64 PulseAudioPlayer::GetCurrentPosition() { - pa_sample_spec ss; - ss.format = PA_SAMPLE_S16LE; - ss.channels = provider->GetChannels(); - ss.rate = provider->GetSampleRate(); - - s = pa_simple_new( - NULL, // default server - "Aegisub", // application name - PA_STREAM_PLAYBACK, - NULL, // default device - "Sound", // stream description - &ss, // sample format - NULL, // default channel map - NULL, // default buffering attributes - &paerror // store error code - ); - if (!s) throw pa_strerror(paerror); + // TODO: use pulse functions + return cur_frame; } -///////////////////// -// Thread destructor -PulseAudioPlayerThread::~PulseAudioPlayerThread() { - pa_simple_free(s); -} - - -////////////////////// -// Thread entry point -wxThread::ExitCode PulseAudioPlayerThread::Entry() +// Called by PA to notify about contetxt operation completion +void PulseAudioPlayer::pa_context_success(pa_context *c, int success, PulseAudioPlayer *thread) { - // Loop as long as we aren't told to shutdown - while (shutdown_notify.TryWait() == wxSEMA_BUSY) { - // Wait for a playback operation - if (play_notify.WaitTimeout(100) == wxSEMA_NO_ERROR) { - // So playback was posted... now play something - play_mutex.Lock(); - PlaybackLoop(); - play_mutex.Unlock(); - } - cur_frame = cur_frame / 2; + thread->context_success_val = success; + thread->context_success.Post(); +} + + +// Called by PA to notify about other context-related stuff +void PulseAudioPlayer::pa_context_notify(pa_context *c, PulseAudioPlayer *thread) +{ + thread->cstate = pa_context_get_state(thread->context); + //printf("Context state change: %d\n", thread->cstate); + thread->context_notify.Post(); +} + + +// Called by PA when an operation completes +void PulseAudioPlayer::pa_stream_success(pa_stream *p, int success, PulseAudioPlayer *thread) +{ + thread->stream_success_val = success; + thread->stream_success.Post(); +} + + +// Called by PA to request more data (and other things?) +void PulseAudioPlayer::pa_stream_write(pa_stream *p, size_t length, PulseAudioPlayer *thread) +{ + if (!thread->is_playing) return; + if (thread->cur_frame >= thread->end_frame) { + thread->is_playing = false; + printf("PA requested more buffer, but no more to stream\n"); + return; } - return 0; + printf("PA requested more buffer, %lu bytes\n", (unsigned long)length); + unsigned long bpf = thread->bpf; + unsigned long frames = length / thread->bpf; + unsigned long maxframes = thread->end_frame - thread->cur_frame; + if (frames > maxframes) frames = maxframes; + printf("Handing it %lu frames\n", frames); + void *buf = malloc(frames * bpf); + thread->provider->GetAudioWithVolume(buf, thread->cur_frame, frames, thread->volume); + ::pa_stream_write(p, buf, frames*bpf, free, 0, PA_SEEK_RELATIVE); + thread->cur_frame += frames; } -void PulseAudioPlayerThread::PlaybackLoop() + +// Called by PA to notify about other stuff +void PulseAudioPlayer::pa_stream_notify(pa_stream *p, PulseAudioPlayer *thread) { - // Bytes per frame - unsigned long bpf = provider->GetChannels() * provider->GetBytesPerSample(); - // Number of frames in a read - unsigned long datalen = provider->GetSampleRate()/8; - // Read buffer - void *data = malloc(datalen * bpf); - unsigned long cur_pos = 0; - - while (true) { - // Get/set parameters - parameter_mutex.Lock(); - double vol = volume; - cur_frame = cur_pos; - unsigned long end_pos = end_frame; - parameter_mutex.Unlock(); - - // Check for stop - if (stop_notify.TryWait() != wxSEMA_BUSY) { - pa_simple_flush(s, &paerror); - break; - } - - // Check for end of stream - if (cur_pos >= end_pos) { - pa_simple_drain(s, &paerror); - break; - } - - // Write some frames - unsigned long to_read = datalen; - if (cur_pos + to_read > end_pos) - to_read = end_pos - cur_pos; - provider->GetAudioWithVolume(data, cur_pos, to_read, vol); - if (pa_simple_write(s, data, to_read*bpf, &paerror) < 0) break; - cur_pos += to_read; - } - - free(data); -} - - -void PulseAudioPlayerThread::Play(unsigned long _start_frame, unsigned long _num_frames) -{ - StopWait(); - - cur_frame = start_frame = _start_frame; - end_frame = start_frame + _num_frames; - - play_notify.Post(); -} - - -void PulseAudioPlayerThread::SetEndPosition(unsigned long _end_frame) -{ - parameter_mutex.Lock(); - end_frame = _end_frame; - parameter_mutex.Unlock(); -} - - -//////////////////////// -// Stop playback thread -void PulseAudioPlayerThread::Stop() -{ - stop_notify.Post(); -} - - -void PulseAudioPlayerThread::StopWait() -{ - stop_notify.Post(); - // Now wait for the play mutex to have been freed - play_mutex.Lock(); - play_mutex.Unlock(); -} - - -void PulseAudioPlayerThread::Shutdown() -{ - Stop(); - shutdown_notify.Post(); -} - - -void PulseAudioPlayerThread::SetVolume(double new_volume) -{ - parameter_mutex.Lock(); - volume = new_volume; - parameter_mutex.Unlock(); -} - - -unsigned long PulseAudioPlayerThread::GetStartPosition() -{ - wxMutexLocker lock(parameter_mutex); - return start_frame; -} - - -unsigned long PulseAudioPlayerThread::GetEndPosition() -{ - wxMutexLocker lock(parameter_mutex); - return end_frame; -} - - -unsigned long PulseAudioPlayerThread::GetCurrentPosition() -{ - // Step 1: Get last written frame number - parameter_mutex.Lock(); - unsigned long pos = cur_frame; - parameter_mutex.Unlock(); - - // Step 2: ??? - - // Step 3: Profit! - return pos; -} - - -bool PulseAudioPlayerThread::IsPlaying() -{ - return play_mutex.TryLock() == wxMUTEX_BUSY; + thread->sstate = pa_stream_get_state(thread->stream); + //printf("Stream state change: %d\n", thread->sstate); + thread->stream_notify.Post(); } +