From 7743647460575fd9ea4d5e29e5a23e3faf56c58c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Revol?= Date: Thu, 17 Sep 2015 22:18:16 +0200 Subject: output: add native Haiku audio output and mixer support Also uses the notification system to display tags. --- src/output/plugins/HaikuOutputPlugin.cxx | 532 +++++++++++++++++++++++++++++++ src/output/plugins/HaikuOutputPlugin.hxx | 34 ++ 2 files changed, 566 insertions(+) create mode 100644 src/output/plugins/HaikuOutputPlugin.cxx create mode 100644 src/output/plugins/HaikuOutputPlugin.hxx (limited to 'src/output/plugins') diff --git a/src/output/plugins/HaikuOutputPlugin.cxx b/src/output/plugins/HaikuOutputPlugin.cxx new file mode 100644 index 000000000..e235d595a --- /dev/null +++ b/src/output/plugins/HaikuOutputPlugin.cxx @@ -0,0 +1,532 @@ +/* + * Copyright (C) 2003-2015 The Music Player Daemon Project + * http://www.musicpd.org + * Copyright (C) 2014-2015 François 'mmu_man' Revol + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "HaikuOutputPlugin.hxx" +#include "../OutputAPI.hxx" +#include "../Wrapper.hxx" +#include "mixer/MixerList.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "Log.hxx" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define UTF8_PLAY "\xE2\x96\xB6" + +class HaikuOutput { + friend struct AudioOutputWrapper; + friend int haiku_output_get_volume(HaikuOutput &haiku); + friend bool haiku_output_set_volume(HaikuOutput &haiku, unsigned volume); + + AudioOutput base; + + size_t write_size; + + media_raw_audio_format* format; + BSoundPlayer* sound_player; + + sem_id new_buffer; + sem_id buffer_done; + + uint8* buffer; + size_t buffer_size; + size_t buffer_filled; + + unsigned buffer_delay; + +public: + HaikuOutput() + :base(haiku_output_plugin) {} + ~HaikuOutput(); + + bool Initialize(const ConfigBlock &block, Error &error) { + return base.Configure(block, error); + } + + static HaikuOutput *Create(const ConfigBlock &block, Error &error); + + bool Open(AudioFormat &audio_format, Error &error); + + void Close() { + DoClose(); + } + + size_t Play(const void *chunk, size_t size, Error &error); + void Cancel(); + + bool Configure(const ConfigBlock &block, Error &error); + + size_t Delay(); + + void FillBuffer(void* _buffer, size_t size, + gcc_unused const media_raw_audio_format& _format); + + void SendTag(const Tag &tag); + +private: + + void DoClose(); +}; + +static constexpr Domain haiku_output_domain("haiku_output"); + +static void +haiku_output_error(Error &error_r, status_t err) +{ + const char *error = strerror(err); + error_r.Set(haiku_output_domain, err, error); +} + +static void +initialize_application() +{ + // required to send the notification with a bitmap + // TODO: actually Run() it and handle B_QUIT_REQUESTED + // TODO: use some locking? + if (be_app == NULL) { + FormatDebug(haiku_output_domain, "creating be_app\n"); + new BApplication("application/x-vnd.MusicPD"); + } +} + +static void +finalize_application() +{ + // TODO: use some locking? + delete be_app; + be_app = NULL; + FormatDebug(haiku_output_domain, "deleting be_app\n"); +} + +inline bool +HaikuOutput::Configure(const ConfigBlock &block, Error &error) +{ + /* XXX: by default we should let the MediaKit propose the buffer size */ + write_size = block.GetBlockValue("write_size", 4096u); + + format = (media_raw_audio_format*)malloc( + sizeof(media_raw_audio_format)); + if (format == nullptr) { + haiku_output_error(error, B_NO_MEMORY); + return false; + } + + return true; +} + +static bool +haiku_test_default_device(void) +{ + BSoundPlayer testPlayer; + return testPlayer.InitCheck() == B_OK; + +} + +inline HaikuOutput * +HaikuOutput::Create(const ConfigBlock &block, Error &error) +{ + initialize_application(); + + HaikuOutput *ad = new HaikuOutput(); + + if (!ad->Initialize(block, error)) { + delete ad; + return nullptr; + } + + if (!ad->Configure(block, error)) { + delete ad; + return nullptr; + } + + return ad; +} + +void +HaikuOutput::DoClose() +{ + sound_player->SetHasData(false); + delete_sem(new_buffer); + delete_sem(buffer_done); + sound_player->Stop(); + delete sound_player; + sound_player = nullptr; +} + + + +HaikuOutput::~HaikuOutput() +{ + free(format); + delete_sem(new_buffer); + delete_sem(buffer_done); + + finalize_application(); +} + +static void +fill_buffer(void* cookie, void* buffer, size_t size, + const media_raw_audio_format& format) +{ + HaikuOutput *ad = (HaikuOutput *)cookie; + ad->FillBuffer(buffer, size, format); +} + + +void +HaikuOutput::FillBuffer(void* _buffer, size_t size, + gcc_unused const media_raw_audio_format& _format) +{ + + buffer = (uint8*)_buffer; + buffer_size = size; + buffer_filled = 0; + bigtime_t start = system_time(); + release_sem(new_buffer); + acquire_sem(buffer_done); + bigtime_t w = system_time() - start; + + if (w > 5000LL) { + FormatDebug(haiku_output_domain, + "haiku:fill_buffer waited %Ldus\n", w); + } + + if (buffer_filled < buffer_size) { + memset(buffer + buffer_filled, 0, + buffer_size - buffer_filled); + FormatDebug(haiku_output_domain, + "haiku:fill_buffer filled %d size %d clearing remainder\n", + (int)buffer_filled, (int)buffer_size); + + } +} + +inline bool +HaikuOutput::Open(AudioFormat &audio_format, Error &error) +{ + status_t err; + *format = media_multi_audio_format::wildcard; + + switch (audio_format.format) { + case SampleFormat::S8: + format->format = media_raw_audio_format::B_AUDIO_CHAR; + break; + + case SampleFormat::S16: + format->format = media_raw_audio_format::B_AUDIO_SHORT; + break; + + case SampleFormat::S32: + format->format = media_raw_audio_format::B_AUDIO_INT; + break; + + case SampleFormat::FLOAT: + format->format = media_raw_audio_format::B_AUDIO_FLOAT; + break; + + default: + /* fall back to float */ + audio_format.format = SampleFormat::FLOAT; + format->format = media_raw_audio_format::B_AUDIO_FLOAT; + break; + } + + format->frame_rate = audio_format.sample_rate; + format->byte_order = B_MEDIA_HOST_ENDIAN; + format->channel_count = audio_format.channels; + + buffer_size = 0; + + if (write_size) + format->buffer_size = write_size; + else + format->buffer_size = BMediaRoster::Roster()->AudioBufferSizeFor( + format->channel_count, format->format, + format->frame_rate, B_UNKNOWN_BUS) * 2; + + FormatDebug(haiku_output_domain, + "using haiku driver ad: bs: %d ws: %d " + "channels %d rate %f fmt %08lx bs %d\n", + (int)buffer_size, (int)write_size, + (int)format->channel_count, format->frame_rate, + format->format, (int)format->buffer_size); + + sound_player = new BSoundPlayer(format, "MPD Output", + fill_buffer, NULL, this); + + err = sound_player->InitCheck(); + if (err != B_OK) { + delete sound_player; + sound_player = NULL; + haiku_output_error(error, err); + return false; + } + + // calculate the allowable delay for the buffer (ms) + buffer_delay = format->buffer_size; + buffer_delay /= (format->format & + media_raw_audio_format::B_AUDIO_SIZE_MASK); + buffer_delay /= format->channel_count; + buffer_delay *= 1000 / format->frame_rate; + // half of the total buffer play time + buffer_delay /= 2; + FormatDebug(haiku_output_domain, + "buffer delay: %d ms\n", buffer_delay); + + new_buffer = create_sem(0, "New buffer request"); + buffer_done = create_sem(0, "Buffer done"); + + sound_player->SetVolume(1.0); + sound_player->Start(); + sound_player->SetHasData(false); + + return true; +} + +inline size_t +HaikuOutput::Play(const void *chunk, size_t size, gcc_unused Error &error) +{ + BSoundPlayer* const soundPlayer = sound_player; + const uint8 *data = (const uint8 *)chunk; + + if (size == 0) { + soundPlayer->SetHasData(false); + return 0; + } + + if (!soundPlayer->HasData()) + soundPlayer->SetHasData(true); + acquire_sem(new_buffer); + + size_t bytesLeft = size; + while (bytesLeft > 0) { + if (buffer_filled == buffer_size) { + // Request another buffer from BSoundPlayer + release_sem(buffer_done); + acquire_sem(new_buffer); + } + + const size_t copyBytes = std::min(bytesLeft, buffer_size + - buffer_filled); + memcpy(buffer + buffer_filled, data, + copyBytes); + buffer_filled += copyBytes; + data += copyBytes; + bytesLeft -= copyBytes; + } + + + if (buffer_filled < buffer_size) { + // Continue filling this buffer the next time this function is called + release_sem(new_buffer); + } else { + // Buffer is full + release_sem(buffer_done); + //soundPlayer->SetHasData(false); + } + + return size; +} + +inline size_t +HaikuOutput::Delay() +{ + unsigned delay = buffer_filled ? 0 : buffer_delay; + + //FormatDebug(haiku_output_domain, + // "delay=%d\n", delay / 2); + // XXX: doesn't work + //return (delay / 2) ? 1 : 0; + (void)delay; + + return 0; +} + +inline void +HaikuOutput::SendTag(const Tag &tag) +{ + status_t err; + + /* lazily initialized */ + static BBitmap *icon = NULL; + + if (icon == NULL) { + BAppFileInfo info; + BResources resources; + err = resources.SetToImage((const void *)&HaikuOutput::SendTag); + BFile file(resources.File()); + err = info.SetTo(&file); + icon = new BBitmap(BRect(0, 0, (float)B_LARGE_ICON - 1, + (float)B_LARGE_ICON - 1), B_BITMAP_NO_SERVER_LINK, B_RGBA32); + err = info.GetIcon(icon, B_LARGE_ICON); + if (err != B_OK) { + delete icon; + icon = NULL; + } + } + + BNotification notification(B_INFORMATION_NOTIFICATION); + + BString messageId("mpd_"); + messageId << find_thread(NULL); + notification.SetMessageID(messageId); + + notification.SetGroup("Music Player Daemon"); + + char timebuf[16]; + unsigned seconds = 0; + if (!tag.duration.IsNegative()) { + seconds = tag.duration.ToS(); + snprintf(timebuf, sizeof(timebuf), "%02d:%02d:%02d", + seconds / 3600, (seconds % 3600) / 60, seconds % 60); + } + + BString artist; + BString album; + BString title; + BString track; + BString name; + + for (const auto &item : tag) + { + switch (item.type) { + case TAG_ARTIST: + case TAG_ALBUM_ARTIST: + if (artist.Length() == 0) + artist << item.value; + break; + case TAG_ALBUM: + if (album.Length() == 0) + album << item.value; + break; + case TAG_TITLE: + if (title.Length() == 0) + title << item.value; + break; + case TAG_TRACK: + if (track.Length() == 0) + track << item.value; + break; + case TAG_NAME: + if (name.Length() == 0) + name << item.value; + break; + case TAG_GENRE: + case TAG_DATE: + case TAG_PERFORMER: + case TAG_COMMENT: + case TAG_DISC: + case TAG_COMPOSER: + case TAG_MUSICBRAINZ_ARTISTID: + case TAG_MUSICBRAINZ_ALBUMID: + case TAG_MUSICBRAINZ_ALBUMARTISTID: + case TAG_MUSICBRAINZ_TRACKID: + default: + FormatDebug(haiku_output_domain, + "tag item: type %d value '%s'\n", item.type, item.value); + break; + } + } + + notification.SetTitle(UTF8_PLAY " Now Playing:"); + + BStringList content; + if (name.Length()) + content.Add(name); + if (artist.Length()) + content.Add(artist); + if (album.Length()) + content.Add(album); + if (track.Length()) + content.Add(track); + if (title.Length()) + content.Add(title); + + if (content.CountStrings() == 0) + content.Add("(Unknown)"); + + BString full = content.Join(" " B_UTF8_BULLET " "); + + if (seconds > 0) + full << " (" << timebuf << ")"; + + notification.SetContent(full); + + err = notification.SetIcon(icon); + + notification.Send(); +} + +int +haiku_output_get_volume(HaikuOutput &haiku) +{ + BSoundPlayer* const soundPlayer = haiku.sound_player; + + if (soundPlayer == NULL || soundPlayer->InitCheck() != B_OK) + return 0; + + return (int)(soundPlayer->Volume() * 100 + 0.5); +} + +bool +haiku_output_set_volume(HaikuOutput &haiku, unsigned volume) +{ + BSoundPlayer* const soundPlayer = haiku.sound_player; + + if (soundPlayer == NULL || soundPlayer->InitCheck() != B_OK) + return false; + + soundPlayer->SetVolume((float)volume / 100); + return true; +} + +typedef AudioOutputWrapper Wrapper; + +const struct AudioOutputPlugin haiku_output_plugin = { + "haiku", + haiku_test_default_device, + &Wrapper::Init, + &Wrapper::Finish, + nullptr, + nullptr, + &Wrapper::Open, + &Wrapper::Close, + &Wrapper::Delay, + &Wrapper::SendTag, + &Wrapper::Play, + nullptr, + nullptr, + nullptr, + + &haiku_mixer_plugin, +}; diff --git a/src/output/plugins/HaikuOutputPlugin.hxx b/src/output/plugins/HaikuOutputPlugin.hxx new file mode 100644 index 000000000..fb85d4b83 --- /dev/null +++ b/src/output/plugins/HaikuOutputPlugin.hxx @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2003-2015 The Music Player Daemon Project + * http://www.musicpd.org + * Copyright (C) 2014-2015 François 'mmu_man' Revol + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_HAIKU_OUTPUT_PLUGIN_HXX +#define MPD_HAIKU_OUTPUT_PLUGIN_HXX + +class HaikuOutput; + +extern const struct AudioOutputPlugin haiku_output_plugin; + +int +haiku_output_get_volume(HaikuOutput &haiku); + +bool +haiku_output_set_volume(HaikuOutput &haiku, unsigned volume); + +#endif -- cgit v1.2.3