Skip to content

decoder/psgplay: Support Atari SNDH files via PSG play library#2444

Open
frno7 wants to merge 1 commit intoMusicPlayerDaemon:masterfrom
frno7:master
Open

decoder/psgplay: Support Atari SNDH files via PSG play library#2444
frno7 wants to merge 1 commit intoMusicPlayerDaemon:masterfrom
frno7:master

Conversation

@frno7
Copy link
Contributor

@frno7 frno7 commented Mar 7, 2026

No description provided.

@frno7
Copy link
Contributor Author

frno7 commented Mar 7, 2026

This PSG play MPD plugin for Atari ST and STE SNDH chip music is very similar to the preexisting SID play MPD plugin for C64 chip music.

Note! Although the dependency in src/decoder/plugins/meson.build is provisionally set to the 0.8 tag, a few more patches are needed, so to try it out compile the latest main branch of https://github.com/frno7/psgplay instead. My thinking is to make a 0.9 tag once all the necessary parts for this pull request have been settled. :-)

@MaxKellermann
Copy link
Member

Unfortunately, your library is not compatible with C++:

In file included from /usr/local/stow/psgplay/include/psgplay/psgplay.h:12,
                 from ../../src/decoder/plugins/PsgplayDecoderPlugin.cxx:20:
/usr/local/stow/psgplay/include/psgplay/digital.h:36:16: error: ISO C++ prohibits anonymous structs [-Werror=pedantic]
   36 |         struct { PSGPLAY_BITFIELD(uint8_t u4 : 4, PSGPLAY_BITFIELD(uint8_t : 4, ;)) };
      |                ^
/usr/local/stow/psgplay/include/psgplay/digital.h:37:16: error: ISO C++ prohibits anonymous structs [-Werror=pedantic]
   37 |         struct { PSGPLAY_BITFIELD(uint8_t u5 : 5, PSGPLAY_BITFIELD(uint8_t : 3, ;)) };
      |                ^
/usr/local/stow/psgplay/include/psgplay/digital.h:38:16: error: ISO C++ prohibits anonymous structs [-Werror=pedantic]
   38 |         struct { PSGPLAY_BITFIELD(uint8_t u6 : 6, PSGPLAY_BITFIELD(uint8_t : 2, ;)) };
      |                ^
/usr/local/stow/psgplay/include/psgplay/digital.h:39:16: error: ISO C++ prohibits anonymous structs [-Werror=pedantic]
   39 |         struct { PSGPLAY_BITFIELD(uint8_t u7 : 7, PSGPLAY_BITFIELD(uint8_t : 1, ;)) };
      |                ^
In file included from ../../src/decoder/plugins/PsgplayDecoderPlugin.cxx:21:
/usr/local/stow/psgplay/include/psgplay/sndh.h:315:25: error: ISO C++ forbids flexible array member ‘data’ [-Werror=pedantic]
  315 |                 uint8_t data[];
      |                         ^~~~
/usr/local/stow/psgplay/include/psgplay/sndh.h:316:12: error: invalid use of ‘struct sndh_file::<unnamed>’ with a flexible array member in ‘struct sndh_file’ -Werror=pedantic]
  316 |         } *sndh;
      |            ^~~~
/usr/local/stow/psgplay/include/psgplay/sndh.h:315:25: note: array member ‘uint8_t sndh_file::<unnamed struct>::data []’ declared here
  315 |                 uint8_t data[];
      |                         ^~~~
/usr/local/stow/psgplay/include/psgplay/sndh.h: In function ‘sndh_timer u32_to_sndh_timer(uint32_t)’:
/usr/local/stow/psgplay/include/psgplay/sndh.h:329:9: error: ISO C++ forbids compound-literals [-Werror=pedantic]
  329 |         };
      |         ^
/usr/local/stow/psgplay/include/psgplay/sndh.h:328:36: error: narrowing conversion of ‘(((unsigned int)timer) >> 8)’ from ‘unsigned int’ to ‘int’ [-Werror=narrowing]
  328 |                 .frequency = timer >> 8,
      |                              ~~~~~~^~~~

I'm unable to build MPD with your code.

@frno7
Copy link
Contributor Author

frno7 commented Mar 11, 2026

Unfortunately, your library is not compatible with C++: /.../ I'm unable to build MPD with your code.

What C++ compiler and version are you using? I’m able to successfully compile it with

% c++ --version
c++ (Gentoo 14.3.1_p20250801 p4) 14.3.1 20250801
Copyright (C) 2024 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

as demonstrated with

% touch src/decoder/plugins/PsgplayDecoderPlugin.cxx
% ninja --verbose -C output/release
ninja: Entering directory `output/release'
[1/5] /usr/lib/python-exec/python3.13/meson --internal vcstagger ../../src/GitVersion.cxx GitVersion.cxx 0.25 mpd @VCS_TAG@ '(.*)' git describe --dirty=+ --always
[2/4] c++ -Isrc/decoder/plugins/libdecoder_plugins.a.p -Isrc/decoder/plugins -I../../src/decoder/plugins -Isrc -I../../src -I. -I../.. -I/usr/include/opus -fdiagnostics-color=always -DNDEBUG -D_FILE_OFFSET_BITS=64 -Wall -Winvalid-pch -Wextra -Wpedantic -std=c++23 -O2 -g -ffast-math -ftree-vectorize -Wcast-qual -Wdouble-promotion -Wmissing-declarations -Wshadow -Wunused -Wvla -Wwrite-strings -Wno-stringop-overflow -fno-threadsafe-statics -fmerge-all-constants -Wcomma-subscript -Wextra-semi -Wmismatched-tags -Woverloaded-virtual -Wsign-promo -Wsuggest-override -Wvolatile -Wvirtual-inheritance -Wno-missing-field-initializers -Wno-non-virtual-dtor -fvisibility=hidden -ffunction-sections -fdata-sections -D_GNU_SOURCE -fPIC -Wno-array-bounds -MD -MQ src/decoder/plugins/libdecoder_plugins.a.p/PsgplayDecoderPlugin.cxx.o -MF src/decoder/plugins/libdecoder_plugins.a.p/PsgplayDecoderPlugin.cxx.o.d -o src/decoder/plugins/libdecoder_plugins.a.p/PsgplayDecoderPlugin.cxx.o -c ../../src/decoder/plugins/PsgplayDecoderPlugin.cxx
[3/4] rm -f src/decoder/plugins/libdecoder_plugins.a && gcc-ar csrDT src/decoder/plugins/libdecoder_plugins.a src/decoder/plugins/libdecoder_plugins.a.p/PcmDecoderPlugin.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/DsdiffDecoderPlugin.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/DsfDecoderPlugin.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/DsdLib.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/FfmpegIo.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/FfmpegMetaData.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/FfmpegDecoderPlugin.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/FlacDecoderPlugin.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/FlacInput.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/FlacPcm.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/FlacDomain.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/FlacCommon.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/VorbisDecoderPlugin.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/VorbisDomain.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/OpusDecoderPlugin.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/OpusDomain.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/OpusHead.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/OpusTags.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/OggDecoder.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/OggCodec.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/AudiofileDecoderPlugin.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/GmeDecoderPlugin.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/MadDecoderPlugin.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/MikmodDecoderPlugin.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/ModplugDecoderPlugin.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/ModCommon.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/Mpg123DecoderPlugin.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/SndfileDecoderPlugin.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/WildmidiDecoderPlugin.cxx.o src/decoder/plugins/libdecoder_plugins.a.p/PsgplayDecoderPlugin.cxx.o
[4/4] c++  -o mpd mpd.p/meson-generated_.._GitVersion.cxx.o mpd.p/src_Main.cxx.o mpd.p/src_protocol_ArgParser.cxx.o mpd.p/src_protocol_IdleFlags.cxx.o mpd.p/src_command_CommandError.cxx.o mpd.p/src_command_PositionArg.cxx.o mpd.p/src_command_AllCommands.cxx.o mpd.p/src_command_QueueCommands.cxx.o mpd.p/src_command_TagCommands.cxx.o mpd.p/src_command_PlayerCommands.cxx.o mpd.p/src_command_PlaylistCommands.cxx.o mpd.p/src_command_FileCommands.cxx.o mpd.p/src_command_OutputCommands.cxx.o mpd.p/src_command_MessageCommands.cxx.o mpd.p/src_command_ClientCommands.cxx.o mpd.p/src_command_PartitionCommands.cxx.o mpd.p/src_command_OtherCommands.cxx.o mpd.p/src_command_CommandListBuilder.cxx.o mpd.p/src_config_PartitionConfig.cxx.o mpd.p/src_config_PlayerConfig.cxx.o mpd.p/src_config_ReplayGainConfig.cxx.o mpd.p/src_Idle.cxx.o mpd.p/src_decoder_Thread.cxx.o mpd.p/src_decoder_Control.cxx.o mpd.p/src_decoder_Bridge.cxx.o mpd.p/src_decoder_DecoderPrint.cxx.o mpd.p/src_client_Listener.cxx.o mpd.p/src_client_Client.cxx.o mpd.p/src_client_Config.cxx.o mpd.p/src_client_Domain.cxx.o mpd.p/src_client_Event.cxx.o mpd.p/src_client_Expire.cxx.o mpd.p/src_client_Idle.cxx.o mpd.p/src_client_List.cxx.o mpd.p/src_client_New.cxx.o mpd.p/src_client_Process.cxx.o mpd.p/src_client_Read.cxx.o mpd.p/src_client_Write.cxx.o mpd.p/src_client_Message.cxx.o mpd.p/src_client_Subscribe.cxx.o mpd.p/src_client_File.cxx.o mpd.p/src_client_Response.cxx.o mpd.p/src_client_ThreadBackgroundCommand.cxx.o mpd.p/src_client_ProtocolFeature.cxx.o mpd.p/src_client_StringNormalization.cxx.o mpd.p/src_Listen.cxx.o mpd.p/src_LogInit.cxx.o mpd.p/src_ls.cxx.o mpd.p/src_Instance.cxx.o mpd.p/src_MusicBuffer.cxx.o mpd.p/src_MusicPipe.cxx.o mpd.p/src_MusicChunk.cxx.o mpd.p/src_MusicChunkPtr.cxx.o mpd.p/src_Mapper.cxx.o mpd.p/src_Partition.cxx.o mpd.p/src_Permission.cxx.o mpd.p/src_player_CrossFade.cxx.o mpd.p/src_player_Thread.cxx.o mpd.p/src_player_Control.cxx.o mpd.p/src_PlaylistError.cxx.o mpd.p/src_PlaylistPrint.cxx.o mpd.p/src_PlaylistSave.cxx.o mpd.p/src_playlist_Length.cxx.o mpd.p/src_playlist_PlaylistStream.cxx.o mpd.p/src_playlist_PlaylistMapper.cxx.o mpd.p/src_playlist_PlaylistAny.cxx.o mpd.p/src_playlist_PlaylistSong.cxx.o mpd.p/src_playlist_PlaylistQueue.cxx.o mpd.p/src_playlist_Print.cxx.o mpd.p/src_db_PlaylistVector.cxx.o mpd.p/src_queue_Queue.cxx.o mpd.p/src_queue_Print.cxx.o mpd.p/src_queue_Save.cxx.o mpd.p/src_queue_Selection.cxx.o mpd.p/src_queue_Playlist.cxx.o mpd.p/src_queue_PlaylistControl.cxx.o mpd.p/src_queue_PlaylistEdit.cxx.o mpd.p/src_queue_PlaylistTag.cxx.o mpd.p/src_queue_PlaylistState.cxx.o mpd.p/src_LocateUri.cxx.o mpd.p/src_SongUpdate.cxx.o mpd.p/src_SongLoader.cxx.o mpd.p/src_SongPrint.cxx.o mpd.p/src_SongSave.cxx.o mpd.p/src_StateFile.cxx.o mpd.p/src_StateFileConfig.cxx.o mpd.p/src_Stats.cxx.o mpd.p/src_TagPrint.cxx.o mpd.p/src_TagSave.cxx.o mpd.p/src_TagFile.cxx.o mpd.p/src_TagStream.cxx.o mpd.p/src_TagAny.cxx.o mpd.p/src_TimePrint.cxx.o mpd.p/src_mixer_Memento.cxx.o mpd.p/src_PlaylistFile.cxx.o mpd.p/src_CommandLine.cxx.o mpd.p/src_unix_SignalHandlers.cxx.o mpd.p/src_unix_Daemon.cxx.o mpd.p/src_storage_StorageState.cxx.o mpd.p/src_queue_PlaylistUpdate.cxx.o mpd.p/src_command_StorageCommands.cxx.o mpd.p/src_command_DatabaseCommands.cxx.o mpd.p/src_RemoteTagCache.cxx.o mpd.p/src_command_StickerCommands.cxx.o mpd.p/src_sticker_Database.cxx.o mpd.p/src_sticker_Print.cxx.o mpd.p/src_sticker_SongSticker.cxx.o mpd.p/src_sticker_TagSticker.cxx.o mpd.p/src_sticker_AllowedTags.cxx.o mpd.p/src_sticker_CleanupService.cxx.o mpd.p/src_command_NeighborCommands.cxx.o mpd.p/src_TagArchive.cxx.o mpd.p/src_db_update_Archive.cxx.o -Wl,--as-needed -Wl,--no-undefined -Wl,-z,relro -Wl,-z,now -Wl,--gc-sections -Wl,--start-group src/cmdline/libcmdline.a src/lib/fmt/libfmt.a libbasic.a src/config/libfs.a liblog.a src/fs/libfs.a src/io/libio.a src/lib/icu/libicu.a src/util/libutil.a src/fs/glue/libfs_glue.a src/io/fs/libio_fs.a src/lib/dbus/libdbus.a src/event/libevent.a src/io/uring/liburing.a src/thread/libthread.a src/net/libnet.a src/system/libsystem.a src/neighbor/libneighbor_glue.a src/neighbor/plugins/libneighbor_plugins.a src/input/libinput_glue.a src/input/libinput_api.a src/input/libinput_basic.a src/memory/libmemory.a src/input/plugins/libinput_plugins.a src/lib/alsa/libalsa.a src/pcm/libpcm_basic.a src/lib/curl/libcurl.a src/lib/ffmpeg/libffmpeg.a src/lib/ffmpeg/libffmpeg_util.a src/lib/crypto/libcrypto_md5.a src/pcm/libpcm.a src/tag/libtag.a src/time/libtime.a src/archive/libarchive_glue.a src/archive/plugins/libarchive_plugins.a src/archive/libarchive_api.a src/output/liboutput_glue.a src/output/liboutput_registry.a src/output/plugins/liboutput_plugins.a src/lib/pipewire/libpipewire.a src/lib/pulse/libpulse.a src/output/liboutput_api.a src/filter/plugins/libfilter_plugins.a src/filter/libfilter_api.a src/mixer/plugins/libmixer_plugins.a src/mixer/libmixer_api.a src/zeroconf/libzeroconf_avahi.a src/zeroconf/avahi/libavahi.a src/filter/libfilter_glue.a src/mixer/libmixer_glue.a src/decoder/libdecoder_glue.a src/decoder/plugins/libdecoder_plugins.a src/lib/xiph/libflac.a src/lib/xiph/libxiph.a src/lib/xiph/libvorbis.a src/lib/xiph/libogg.a src/lib/crypto/libcrypto_base64.a src/decoder/libdecoder_api.a src/encoder/libencoder_glue.a src/encoder/plugins/libencoder_plugins.a src/playlist/libplaylist_glue.a src/playlist/plugins/libplaylist_plugins.a src/lib/expat/libexpat.a src/playlist/libplaylist_api.a src/db/libdb_glue.a src/db/plugins/libdb_plugins.a src/lib/pcre/libpcre.a src/lib/zlib/libzlib.a src/db/libdb_api.a src/storage/libstorage_api.a src/storage/libstorage_glue.a src/storage/plugins/libstorage_plugins.a src/song/libsong.a src/lib/sqlite/libsqlite.a /usr/lib64/libdbus-1.so /usr/lib64/libfmt.so /usr/lib64/liburing.so /usr/lib64/libavutil.so /usr/lib64/libavformat.so /usr/lib64/libavcodec.so /usr/lib64/libavfilter.so /usr/lib64/libpcre2-8.so /usr/lib64/libsqlite3.so /usr/lib64/libicui18n.so /usr/lib64/libicuuc.so -pthread /usr/lib64/libcurl.so /usr/lib64/libcdio_paranoia.so /usr/lib64/libcdio_cdda.so /usr/lib64/libcdio.so -lm /usr/lib64/libasound.so /usr/lib64/libsamplerate.so /usr/lib64/libsoxr.so /usr/lib64/libid3tag.so /usr/lib64/libz.so -lbz2 /usr/lib64/libiso9660.so /usr/lib64/libzzip.so /usr/lib64/libpipewire-0.3.so /usr/lib64/libpulse.so /usr/lib64/libavahi-common.so /usr/lib64/libavahi-client.so /usr/lib64/libFLAC.so /usr/lib64/libaudiofile.so /usr/lib64/libgme.so -lmad /usr/lib64/libmikmod.so /usr/lib64/libmodplug.so /usr/lib64/libmpg123.so /usr/lib64/libopus.so /usr/lib64/libpsgplay.so /usr/lib64/libsndfile.so /usr/lib64/libogg.so /usr/lib64/libvorbis.so /usr/lib64/libWildMidi.so /usr/lib64/libvorbisenc.so -lmp3lame /usr/lib64/libtwolame.so /usr/lib64/libexpat.so /usr/lib64/libmpdclient.so -Wl,--end-group

@MaxKellermann
Copy link
Member

My compiler is the standard GCC from Debian stable:

c++ (Debian 14.2.0-19) 14.2.0

But this really isn't about the compiler. The code in your library headers is not valid C++.

@MaxKellermann
Copy link
Member

I removed lots of useless code from your library headers and then your headers did build in C++ mode.

Turns out that all these C++-incompatible defintions in your headers are unused. I say they simply do not belong there; they should not be part of the public API of your library.

@frno7
Copy link
Contributor Author

frno7 commented Mar 11, 2026

But this really isn't about the compiler. The code in your library headers is not valid C++.

Indeed, but I’d like to be able to reproduce these errors, to verify I’ve fixed them properly. :-) Without being able to reproduce the error, I’m in the blind. Both our compilers are GCC 14, but mine is slightly newer (14.3) than yours (14.2). Have you configured your build environment in any way?

The only fix for C++ and GCC 14.3.1 I had to do last week (before submitting this pull request) was commit frno7/psgplay@8ed589e, which presumably you’ve already got. The problem with the GCC 14.2.0 C++ compiler refusing anonymous structs and flexible array members certainly look fixable.

@MaxKellermann
Copy link
Member

What was your meson setup command?

@frno7
Copy link
Contributor Author

frno7 commented Mar 11, 2026

I removed lots of useless code from your library headers and then your headers did build in C++ mode.

Turns out that all these C++-incompatible defintions in your headers are unused. I say they simply do not belong there; they should not be part of the public API of your library.

Heh, well, MPD certainly doesn’t use the whole public API. As briefly mentioned about the API, apart from the ‘simple’ stereo audio API (that the MPD plugin uses), there’s also a more advanced, low-level, 250 kHz ‘digital’ API that more directly interfaces the sound chip YM2149 hardware, in case people want to make their own custom analogue filters, display popular three-channel oscilloscope effects in their players, etc. (hence the errors from include/psgplay/digital.h you had).

What was your meson setup command?

I followed the official MPD guide on compiling from source, with the addition that I had -Dpsgplay=true, so meson setup . output/release --buildtype=debugoptimized -Db_ndebug=true -Dpsgplay=true.


#include <fmt/format.h>

#include <fstream>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using iostreams is not acceptable in MPD. iostreams is a horrible library that adds huge amounts of bloat, both to the code and to runtime.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What file reading (and/or perhaps streaming?) operation would you recommend as a replacement within the MPD plugin? The PSG play library itself doesn’t have any functions to read (nor write) any files: it needs a (complete) array of bytes containing the SNDH music data.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MPD has two APIs to read files:

  • FileReader is a low-level wrapper for a file descriptor/handle
  • InputStream is the generic streaming API that MPD uses to access both local files, remote files (NFS, WebDav etc.) and actual streams (HTTP, HLS)

Since there appears to be no way around reading the whole file, I suggest you switch to MPD's stream decoder API, because that will allow your plugin to read files from anywhere, not just local files.
But instead of InputStream::Read(), decoders should call the decoder_read*() wrappers because they make the read operation cancellable (if the user presses "stop", it will cancel the read instead of having to wait until your NFS server returns all the data).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest you switch to MPD's stream decoder API

Other, preexisting, plugins (GME, SID, VGM), using the WithContainer concept (taking Path rather than InputStream), don’t seem to use InputStream? They also manipulate Path objects to suffix /tune_xxx.* subtunes, etc. Is a stream’s URI the closest thing to a path for the purpose of making containers (having subtunes)?

decoders should call the decoder_read*() wrappers

It seems a (new) decoder_read_whole() would be useful, to read and return a complete file via InputStream, also for the preexisting SID plugin (which currently doesn’t seem to be streamable)?

static unsigned
psgplay_subtune_track(const char *base) noexcept
{
if (memcmp(base, SUBTUNE_PREFIX, sizeof(SUBTUNE_PREFIX) - 1) != 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out-of-bounds read here. Do you know how memcmp() works?
I suggest using StringStartsWith(), or better: StringAfterPrefix(). It does what you want, but without the out-of-bounds read and simpler, less error prone.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is exactly what the preexisting MPD SID play plugin already has:

static unsigned
ParseSubtuneName(const char *base) noexcept
{
if (memcmp(base, SUBTUNE_PREFIX, sizeof(SUBTUNE_PREFIX) - 1) != 0)
return 0;

I agree it isn’t pretty, but there’s value in using the same construct for exactly the same purpose in both plugins, isn’t it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! This piece of code is indeed just as bad and should be rewritten.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!stream)
return { };

return std::vector<uint8_t>((std::istreambuf_iterator<char>(stream)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the file weighs one terabyte? This would certainly crash MPD (and make your computer unusable for an hour or so).

Also std::vector is a lousy choice here. If you read even just a megabyte, it will first zero-initialize the whole allocation and then read into it. The initialization is useless bloat.

Is there is reason you need to slurp the whole file into memory? Can't you stream the file, like the other decoders do?
If you really need to do that, use AllocatedArray<std::byte> (std::byte is the canonical type for bytes).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the file weighs one terabyte?

Atari hardware only had a 24-bit address bus, limiting addressable space to 16 MiB (at most). We can have that as a software limit in the plugin, if you like?

Is there is reason you need to slurp the whole file into memory? Can't you stream the file, like the other decoders do?

As mentioned on how playing SNDH works, the SNDH file format is an Atari ST machine code executable form of music. A substantial part of Atari ST hardware must be emulated to play such files using other kinds of computers. This is exactly the same principle as the preexisting SID play plugin. The ‘music’ is 68000 machine code, and so it’s not made to be ‘streamed’ in a conventional way (with a ‘beginning’ or ‘end’ to files).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like a reasonable limit.

std::ios::in | std::ios::binary);

if (!stream)
return { };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bad error handling. If you can't open a file, throw an exception (that will be handled/logged somewhere down the call stack), but don't fail silently.
This function isn't documented; there's no documentation saying that errors return an empty vector (and I don't think this is a good idea).

{
const auto container = psgplay_container_from_path(path_fs);

const AudioFormat audio_format(44100, SampleFormat::S16, 2);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be static constexpr; that would eliminate the code to allocate stack space and initialize it completely, and instead be just a few passive bytes in .rodata.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do, thanks!

enum { N = 4096 };
struct psgplay_stereo buffer[N];

/* psgplay_read_stereo returns the number of samples */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this (PCM) samples or (stereo) frames?
One (stereo) frame consists of two PCM samples.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return value is the number of stereo sample pairs, which is to say, the number of MPD stereo frames. I can clarify the documentation to reflect this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please. Optimizing for minimum confusion is always worth the effort.

container.track,
audio_format.sample_rate);
if (!pp)
return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This calls psgplay_free(nullptr) which is illegal according to your API documentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This calls psgplay_free(nullptr) which is illegal according to your API documentation.

Hmm... where does it say NULL is invalid? NULL is actually very much permitted, and handled accordingly. This is exactly the way the C standard library handles NULL in free(void *p): If p is NULL, no operation is performed. However, I can certainly add this statement to the validity of NULL to the API documentation of psgplay_free.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/**
 * psgplay_free - free a PSG play object previously initialised
 * @pp: PSG play object to free
 */
void psgplay_free(struct psgplay *pp);

The null pointer is not "a PSG play object previously initialised".
Even if your code handles it, your API documentation disallows it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, indeed. I will add the validity of NULL to the documentation of psgplay_free, thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit frno7/psgplay@58e451a.

const ssize_t k_samples =
psgplay_read_stereo(pp, nullptr,
s_samples - t_samples);
if (k_samples <= 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this stop playback if I seek back to the beginning of a track?
(s_samples=0, t_samples=0, therefore k_samples must be 0 as well)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, I’ve made this paragraph of code conditional on if (s_frames > t_frames) { ... } now (as I’ve replaced samples with frames for clarity on the stereo pair of samples).

@MaxKellermann
Copy link
Member

MaxKellermann commented Mar 11, 2026

hence the errors from include/psgplay/digital.h you had

But why is there no way to use your library without including this one header? I commented out the #include lines from your other headers and it turned out none of the other headers needed it. Include-what-you-use! (But not more.)

I followed the official MPD guide on compiling from source, with the addition that I had -Dpsgplay=true, so meson setup . output/release --buildtype=debugoptimized -Db_ndebug=true -Dpsgplay=true.

So you did not reduce the Meson warning_level? What is its effective value in your setup?

(btw. these are instructions for users. Developers should not use -Db_ndebug=true as this will exclude all debugging code from the build. And --werror might be helpful, too.)

@frno7
Copy link
Contributor Author

frno7 commented Mar 11, 2026

But why is there no way to use your library without including this one header? I commented out the #include lines from your other headers and it turned out none of the other headers needed it. Include-what-you-use! (But not more.)

The stereo API (psgplay/stereo.h) has options to apply various digital filters, to adjust tone channel mixing tables, tone channel balances, and tone channel volumes, that all need struct psgplay_digital defined in the digital API (psgplay/digital.h). Hence, the stereo API depends on (parts of) the digital API (which itself doesn’t depend on anything else in the PSG play library).

However, these digital stereo filters are somewhat work-in-progress, and as it turns out, they need to be reworked because people want to compose multiple filters, in filter chains, which the current stereo filter API won’t allow. I’ve deliberately refrained from using these advanced features in the MPD plugin, until the matter of configuring stereo filter chains has been resolved satisfactorily. Given that the library is version 0.8 as of now, a filter rework might be due for 2.0 or something like that. :-)

The situation is similar with psgplay/sndh.h, the API for parsing SNDH tags. There are ‘simple’ high-level functions for parsing titles, composer names, etc. that return ready-to-use UTF-8 for MPD and other modern players. This avoids ICU conversions issues (for the Atari ST character set), as seen in the SID play plugin:

Windows1252ToUTF8(const char *s) noexcept
{
#ifdef HAVE_ICU_CONVERTER
try {
return IcuConverter::Create("windows-1252")->ToUTF8(s);
} catch (...) { }
#endif

However, SNDH tags can also be parsed at a lower level, for example using the sndh_for_each_tag as seen in lib/example/example-info.c.

So you did not reduce the Meson warning_level? What is its effective value in your setup?

Not consciously, effectively:

% meson configure output/release | grep warning_level
  warning_level       3      [0, 1, 2, 3, everything]         Compiler warning level to use
  warning_level       3      [0, 1, 2, 3, everything]         Compiler warning level to use

What warning level would you expect for triggering the errors you had with your compiler?

(btw. these are instructions for users. Developers should not use -Db_ndebug=true as this will exclude all debugging code from the build. And --werror might be helpful, too.)

Ah, thanks!

@MaxKellermann
Copy link
Member

The stereo API (psgplay/stereo.h) has options to apply various digital filters, to adjust tone channel mixing tables, tone channel balances, and tone channel volumes, that all need struct psgplay_digital defined in the digital API (psgplay/digital.h).

The stereo.h header does not need the digital.h header at all. I commented out the line and everything still worked. That was my point.

Sure, there are optional features, and if a program wants to use them, it needs to include digital.h.

Not consciously, effectively:

That's the default level (the highest one), and the same I used. That was my last idea to explain why I had pedantic warnings enabled but you had not.

@frno7
Copy link
Contributor Author

frno7 commented Mar 11, 2026

The stereo.h header does not need the digital.h header at all. I commented out the line and everything still worked. That was my point.

Hmm. If I simply (only) remove #include "digital.h" I get the following (expected) compiler error for stereo.h:

include/psgplay/stereo.h:45:53: warning: ‘struct psgplay_digital’ declared inside parameter list will not be visible outside of this definition or declaration
   45 |         struct psgplay_stereo *stereo, const struct psgplay_digital *digital,
      |                                                     ^~~~~~~~~~~~~~~

Sure, there are optional features, and if a program wants to use them, it needs to include digital.h.

Maybe the filter stuff could go into a new filter.h, or somesuch, but that whole thing is a bit experimental...

That's the default level (the highest one), and the same I used. That was my last idea to explain why I had pedantic warnings enabled but you had not.

According to #2444 (comment) I actually do have -Wall -Winvalid-pch -Wextra -Wpedantic -std=c++23 among other flags, so -Wpedantic is absolutely there. The offending includes are wrapped in a proper extern "C" { ... } directive, so the usual C++ rules do not really apply for them. I’m looking through GCC bugs resolved between 14.2 (yours) and 14.3 (mine), but it’s quite a lot of stuff in-between these two GCC versions, and maybe even more than shown in the GCC Bugzilla. Anyway, I wonder how I can trigger the same errors as you do.

@MaxKellermann
Copy link
Member

Hmm. If I simply (only) remove #include "digital.h" I get the following (expected) compiler error for stereo.h:

include/psgplay/stereo.h:45:53: warning: ‘struct psgplay_digital’ declared inside parameter list will not be visible outside of this definition or declaration
   45 |         struct psgplay_stereo *stereo, const struct psgplay_digital *digital,
      |                                                     ^~~~~~~~~~~~~~~

Strangely, I don't get that warning, but you can fix it easily by adding a forward declaration for that struct. Just a line saying struct psgplay_digital;.
Keeping header dependencies low is a good idea for many reasons, and forward declarations help with that.

@frno7
Copy link
Contributor Author

frno7 commented Mar 11, 2026

Keeping header dependencies low is a good idea for many reasons, and forward declarations help with that.

Yeah, true.

Maybe the simplest (general) fix is to wrap the optional parts of the API in #if !defined(__cplusplus) ... #endif directives. I have a Github action workflow on the ‘latest’ Ubuntu (as well as Mac OS). I should add a C++ compiler test to the workflow, to catch any C++ problems before they affect the MPD plugin.

@MaxKellermann
Copy link
Member

Sure, if some code in your headers is not valid C++ code, you should protect it with a preprocessor directive. But in this particular case, MPD needs none of this, and keeping internal header dependencies straight is enough to make the problem go away.
Of course, CI validation with a (strict/pedantic) C++ compiler is a good idea.
And it would certainly be just as good an idea to enable your plugin in MPD's CI build. Can we integrate your library as a Meson wrap? (Until there's a Debian/Ubuntu package available in GH Actions)

@frno7
Copy link
Contributor Author

frno7 commented Mar 11, 2026

Sure, if some code in your headers is not valid C++ code, you should protect it with a preprocessor directive.

Fixed in commit frno7/psgplay@9062f5f, where I don’t think there was much choice for the SNDH tag API at the moment.

But in this particular case, MPD needs none of this, and keeping internal header dependencies straight is enough to make the problem go away.

Fixed in commits frno7/psgplay@01891df and frno7/psgplay@71f37a1, thanks!

Of course, CI validation with a (strict/pedantic) C++ compiler is a good idea.

Fixed in commit frno7/psgplay@874e1e7.

And it would certainly be just as good an idea to enable your plugin in MPD's CI build. Can we integrate your library as a Meson wrap? (Until there's a Debian/Ubuntu package available in GH Actions)

I suppose we can! Apart from cloning https://github.com/frno7/psgplay recursively, it only needs a make install command, possibly with a suitable argument prefix=/where/to/install/it. Where would you like to git clone and make install it, in a Meson file somewhere?

@MaxKellermann
Copy link
Member

Where would you like to git clone and make install it, in a Meson file somewhere?

No, certainly not clone from within a Meson file! Either we integrate it as GH Actions shell commands (and checkout with a proper GH action), or add it as a Meson Wrap.
A Meson Wrap may point to a git repository, but it requires using Meson to actually build the subproject (Meson can also build CMake subprojects, but not Make or Autotools or other build systems). Many Wrap authors write a minimal meson.build for projects that don't use Meson already. The Wrap method has a major advantage: it works independent of GH; your library will be available to anybody who builds MPD, without having to mess with make install.

(I suppose somebody like you who put so much time into writing a huuuuge Makefile can't be convinced to switch to Meson completely...)

@frno7
Copy link
Contributor Author

frno7 commented Mar 12, 2026

No, certainly not clone from within a Meson file! Either we integrate it as GH Actions shell commands (and checkout with a proper GH action), or add it as a Meson Wrap. A Meson Wrap may point to a git repository, but it requires using Meson to actually build the subproject (Meson can also build CMake subprojects, but not Make or Autotools or other build systems).

Hmm. So something like

[wrap-git]
directory = psgplay.git
url = https://github.com/frno7/psgplay
revision = v0.9
depth = 1

then, in a psgplay.wrap file, in the subprojects directory of MPD?

Many Wrap authors write a minimal meson.build for projects that don't use Meson already. The Wrap method has a major advantage: it works independent of GH; your library will be available to anybody who builds MPD, without having to mess with make install.

What kind of minimal meson.build did you have in mind? Since it’s not going to install anything, I assume it only needs to run make -j lib/psgplay/libpsgplay.a to produce the static library. In addition, PSG play will want to know what version it is; normally that’s determined by script/version, which either reads a version file in the root of the source directory (for cases like tar archives of the sources), or does git describe --always --tags to determine the relevant tag for itself. I’m not sure whether depth = 1 in the wrap retains tags, in which case a version file containing 0.9 (or whatever revision was specified in the psgplay.wrap file) should be made in the psgplay source directory.

(I suppose somebody like you who put so much time into writing a huuuuge Makefile can't be convinced to switch to Meson completely...)

If Make is good enough for the Linux kernel build system, it’s good enough for me. If they switch to something else (such as Meson), I might follow suit. I suppose it might happen just about when MPD itself makes a wholesale replacement of its C++ with Rust, the language du jour, hmm? ;-)

@MaxKellermann
Copy link
Member

Hmm. So something like

Yes.

What kind of minimal meson.build did you have in mind? Since it’s not going to install anything, I assume it only needs to run make -j lib/psgplay/libpsgplay.a to produce the static library.

No, it should not run make. If you use Meson only to call into a different build tool, it's missing the point. All of your Makefiles need to be ignored. This meson.build should contain a static_library directive that includes all sources that need to be compiled.

If Make is good enough for the Linux kernel build system, it’s good enough for me.

Ha, Linux kbuild is horrible enough to keep me away from make! ;-)
Yet make and Meson run on different abstraction layers. make is just a low-level command execution tool with dependencies; everybody who uses it does it differently (How to pass build-time options? How to use different CFLAGS? How to cross-compile? Where to install? How do I install into a chroot? .... - different for every project) - Meson is an (opinionated) high-level build system which produces commands for a make-like low-level tool. A replacement for autotools, but not for make.
(There was a time when I enjoyed autotools, long ago. Yes, really. That predates cmake and Meson by many years.)

when MPD itself makes a wholesale replacement of its C++ with Rust, the language du jour, hmm? ;-)

Good one!

@frno7
Copy link
Contributor Author

frno7 commented Mar 12, 2026

No, it should not run make. If you use Meson only to call into a different build tool, it's missing the point. All of your Makefiles need to be ignored. This meson.build should contain a static_library directive that includes all sources that need to be compiled.

Hmm... That would inflict Meson on multiple subrepos, having additional source files, so we’d be leaving ‘minimal territory’ for a ‘grand enterprise’. The path forward seems to be the alternative Github action shell commands, then?

@MaxKellermann
Copy link
Member

There are 35 objects in the static library. That's not many, and build complexity doesn't come from the number of source files. A well-designed library with 35 sources can probably be built with a 40-line meson.build file (one source file per line plus a few lines boilerplate).

But of course, you're not a Meson fanboy like me, and having it only in a GH-Action is better than having no CI test. Yet the Meson wrap would have made your library available to all MPD users without them having to care about your specific make-based build system.

@MaxKellermann
Copy link
Member

Note: I don't suggest integrating this into GHActions/MesonWrap now. It's fine for me to finish and merge only the decoder plugin now, and figure out CI or Meson integration later in a new PR. Don't let this delay the merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants