summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build.yml10
-rw-r--r--ChangeLog7
-rw-r--r--INSTALL4
-rw-r--r--README.rst11
-rw-r--r--contrib/config-example1
-rw-r--r--contrib/pianobar.114
-rw-r--r--src/config.h2
-rw-r--r--src/libpiano/response.c27
-rw-r--r--src/player.c25
-rw-r--r--src/player.h1
-rw-r--r--src/ui.c107
-rw-r--r--src/ui_act.c15
12 files changed, 159 insertions, 65 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9b61ea6..536a7d9 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -9,12 +9,14 @@ on:
jobs:
build:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- - name: deps
- run: sudo apt install libao-dev libavcodec-dev libavfilter-dev libavformat-dev libavutil-dev libcurl4-gnutls-dev libgcrypt20-dev libjson-c-dev libpth-dev pkg-config build-essential
- - name: make
+ - name: Install dependencies
+ run: |
+ sudo apt-get update
+ sudo apt install libao-dev libavcodec-dev libavfilter-dev libavformat-dev libavutil-dev libcurl4-gnutls-dev libgcrypt20-dev libjson-c-dev libpth-dev pkg-config build-essential
+ - name: Build pianobar
run: make
diff --git a/ChangeLog b/ChangeLog
index 50b78e1..d36525a 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,10 @@
+Release 2022.04.01
+
+- Not a joke
+- Fix compilation with ffmpeg 5.0 and replace deprecated function
+- Improved retry handling with unreliable HTTP proxies
+- Minor UI improvements
+
Release 2020.11.28
- Support changing station modes
diff --git a/INSTALL b/INSTALL
index 2d36705..fa18296 100644
--- a/INSTALL
+++ b/INSTALL
@@ -7,10 +7,10 @@ Dependencies
- gmake
- pthreads
- libao
-- libcurl
+- libcurl>=7.32.0
- gcrypt[1]
- json-c
-- ffmpeg>=3.3 [2]
+- ffmpeg>=5.1 [2]
- UTF-8 console/locale
[1] with blowfish cipher enabled
diff --git a/README.rst b/README.rst
index 4b137e0..c4d812e 100644
--- a/README.rst
+++ b/README.rst
@@ -33,10 +33,11 @@ and \*BSD as well as a `native Windows port`_.
.. _homebrew: http://brew.sh/
.. _native Windows Port: https://github.com/thedmd/pianobar-windows
-The current pianobar release is 2020.11.28_ (sha256__, sign__). More recent and
+The current pianobar release is 2022.04.01_ (sha256__, sign__). More recent and
experimental code is available at GitHub_ and the local gitweb_. Older releases
are available here:
+- 2020.11.28_ (sha256__, sign__)
- 2020.04.05_ (sha256__, sign__)
- 2019.02.14_ (sha256__, sign__)
- 2019.01.25_ (sha256__, sign__)
@@ -66,12 +67,15 @@ are available here:
- 2010.10.07_ (sha1__)
- 2010.08.21_ (sha1__)
+.. _2022.04.01: https://6xq.net/pianobar/pianobar-2022.04.01.tar.bz2
+__ https://6xq.net/pianobar/pianobar-2022.04.01.tar.bz2.sha256
+__ https://6xq.net/pianobar/pianobar-2022.04.01.tar.bz2.asc
.. _2020.11.28: https://6xq.net/pianobar/pianobar-2020.11.28.tar.bz2
__ https://6xq.net/pianobar/pianobar-2020.11.28.tar.bz2.sha256
__ https://6xq.net/pianobar/pianobar-2020.11.28.tar.bz2.asc
.. _snapshot: http://github.com/PromyLOPh/pianobar/tarball/master
.. _GitHub: http://github.com/PromyLOPh/pianobar/
-.. _gitweb: https://6xq.net/pianobar/git/
+.. _gitweb: https://6xq.net/git/lars/pianobar.git/
.. _2020.04.05: https://6xq.net/pianobar/pianobar-2020.04.05.tar.bz2
__ https://6xq.net/pianobar/pianobar-2020.04.05.tar.bz2.sha256
__ https://6xq.net/pianobar/pianobar-2020.04.05.tar.bz2.asc
@@ -181,10 +185,13 @@ pianobar.el_
Emacs interface for pianobar
`pianobar-mediaplayer2`_
Control pianobar like any other media player through DBUS/MPRIS.
+PianobarNowPlayable_
+ Integrate pianobar with the Now Playing feature of macOS
.. _control-pianobar: http://malabarba.github.io/control-pianobar/
.. _pianobar.el: https://github.com/agrif/pianobar.el
.. _pianobar-mediaplayer2: https://github.com/ryanswilson59/pianobar-mediaplayer2
+.. _PianobarNowPlayable: https://github.com/iDom818/PianobarNowPlayable
Clients
+++++++
diff --git a/contrib/config-example b/contrib/config-example
index 5f5dc2d..060fbcb 100644
--- a/contrib/config-example
+++ b/contrib/config-example
@@ -23,7 +23,6 @@
#act_stationaddbygenre = g
#act_songinfo = i
#act_addshared = j
-#act_songmove = m
#act_songnext = n
#act_songpause = S
#act_songpausetoggle = p
diff --git a/contrib/pianobar.1 b/contrib/pianobar.1
index c5b82aa..887ae4a 100644
--- a/contrib/pianobar.1
+++ b/contrib/pianobar.1
@@ -103,10 +103,6 @@ beginning.
Delete artist/song seeds or feedback.
.TP
-.B act_songmove = m
-Move current song to another station
-
-.TP
.B act_songnext = n
Skip current song.
@@ -449,13 +445,13 @@ can report certain "events" to an external application (see
information like error code and description, was well as song information
related to the current event, is supplied through stdin.
-Currently supported events are: artistbookmark, songban, songbookmark,
-songexplain, songfinish, songlove, songmove, songshelf, songstart,
+Currently supported events are: artistbookmark, settingschange, settingsget,
+songban, songbookmark, songexplain, songfinish, songlove, songshelf, songstart,
stationaddgenre, stationaddmusic, stationaddshared, stationcreate,
stationdelete, stationdeleteartistseed, stationdeletefeedback,
-stationdeletesongseed, stationfetchinfo, stationfetchplaylist,
-stationfetchgenre stationquickmixtoggle, stationrename, userlogin,
-usergetstations
+stationdeletesongseed, stationdeletestationseed, stationfetchgenre,
+stationfetchinfo, stationfetchplaylist, stationgetmodes, stationquickmixtoggle,
+stationrename, stationsetmode, usergetstations, userlogin
An example script can be found in the contrib/ directory of
.B pianobar's
diff --git a/src/config.h b/src/config.h
index ea8a6ec..f3d3d3e 100644
--- a/src/config.h
+++ b/src/config.h
@@ -3,7 +3,7 @@
/* package name */
#define PACKAGE "pianobar"
-#define VERSION "2020.11.28-dev"
+#define VERSION "2022.04.01-dev"
/* glibc feature test macros, define _before_ including other files */
#define _POSIX_C_SOURCE 200809L
diff --git a/src/libpiano/response.c b/src/libpiano/response.c
index 1e79261..0be8872 100644
--- a/src/libpiano/response.c
+++ b/src/libpiano/response.c
@@ -206,7 +206,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) {
break;
}
- for (int i = 0; i < json_object_array_length (stations); i++) {
+ for (unsigned int i = 0; i < json_object_array_length (stations); i++) {
PianoStation_t *tmpStation;
json_object *s = json_object_array_get_idx (stations, i);
@@ -229,7 +229,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) {
if (mix != NULL) {
PianoStation_t *curStation = ph->stations;
PianoListForeachP (curStation) {
- for (int i = 0; i < json_object_array_length (mix); i++) {
+ for (unsigned int i = 0; i < json_object_array_length (mix); i++) {
json_object *id = json_object_array_get_idx (mix, i);
if (strcmp (json_object_get_string (id),
curStation->id) == 0) {
@@ -256,7 +256,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) {
}
assert (items != NULL);
- for (int i = 0; i < json_object_array_length (items); i++) {
+ for (unsigned int i = 0; i < json_object_array_length (items); i++) {
json_object *s = json_object_array_get_idx (items, i);
PianoSong_t *song;
@@ -377,7 +377,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) {
/* get artists */
json_object *artists;
if (json_object_object_get_ex (result, "artists", &artists)) {
- for (int i = 0; i < json_object_array_length (artists); i++) {
+ for (unsigned int i = 0; i < json_object_array_length (artists); i++) {
json_object *a = json_object_array_get_idx (artists, i);
PianoArtist_t *artist;
@@ -396,7 +396,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) {
/* get songs */
json_object *songs;
if (json_object_object_get_ex (result, "songs", &songs)) {
- for (int i = 0; i < json_object_array_length (songs); i++) {
+ for (unsigned int i = 0; i < json_object_array_length (songs); i++) {
json_object *s = json_object_array_get_idx (songs, i);
PianoSong_t *song;
@@ -456,7 +456,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) {
/* get genre stations */
json_object *categories;
if (json_object_object_get_ex (result, "categories", &categories)) {
- for (int i = 0; i < json_object_array_length (categories); i++) {
+ for (unsigned int i = 0; i < json_object_array_length (categories); i++) {
json_object *c = json_object_array_get_idx (categories, i);
PianoGenreCategory_t *tmpGenreCategory;
@@ -471,7 +471,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) {
/* get genre subnodes */
json_object *stations;
if (json_object_object_get_ex (c, "stations", &stations)) {
- for (int k = 0;
+ for (unsigned int k = 0;
k < json_object_array_length (stations); k++) {
json_object *s =
json_object_array_get_idx (stations, k);
@@ -520,12 +520,13 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) {
assert (reqData != NULL);
json_object *explanations;
- if (json_object_object_get_ex (result, "explanations", &explanations)) {
+ if (json_object_object_get_ex (result, "explanations", &explanations) &&
+ json_object_array_length (explanations) > 0) {
reqData->retExplain = malloc (strSize *
sizeof (*reqData->retExplain));
strncpy (reqData->retExplain, "We're playing this track "
"because it features ", strSize);
- for (int i = 0; i < json_object_array_length (explanations); i++) {
+ for (unsigned int i = 0; i < json_object_array_length (explanations); i++) {
json_object *e = json_object_array_get_idx (explanations,
i);
json_object *f;
@@ -573,7 +574,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) {
/* songs */
json_object *songs;
if (json_object_object_get_ex (music, "songs", &songs)) {
- for (int i = 0; i < json_object_array_length (songs); i++) {
+ for (unsigned int i = 0; i < json_object_array_length (songs); i++) {
json_object *s = json_object_array_get_idx (songs, i);
PianoSong_t *seedSong;
@@ -594,7 +595,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) {
/* artists */
json_object *artists;
if (json_object_object_get_ex (music, "artists", &artists)) {
- for (int i = 0; i < json_object_array_length (artists); i++) {
+ for (unsigned int i = 0; i < json_object_array_length (artists); i++) {
json_object *a = json_object_array_get_idx (artists, i);
PianoArtist_t *seedArtist;
@@ -622,7 +623,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) {
continue;
}
assert (json_object_is_type (val, json_type_array));
- for (int i = 0; i < json_object_array_length (val); i++) {
+ for (unsigned int i = 0; i < json_object_array_length (val); i++) {
json_object *s = json_object_array_get_idx (val, i);
PianoSong_t *feedbackSong;
@@ -665,7 +666,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) {
json_object *availableModes;
if (json_object_object_get_ex (result, "availableModes", &availableModes)) {
- for (int i = 0; i < json_object_array_length (availableModes); i++) {
+ for (unsigned int i = 0; i < json_object_array_length (availableModes); i++) {
json_object *val = json_object_array_get_idx (availableModes, i);
assert (json_object_is_type (val, json_type_object));
diff --git a/src/player.c b/src/player.c
index 875f473..753d490 100644
--- a/src/player.c
+++ b/src/player.c
@@ -235,7 +235,7 @@ static bool openStream (player_t * const player) {
softfail ("avcodec_parameters_to_context");
}
- AVCodec * const decoder = avcodec_find_decoder (cp->codec_id);
+ const AVCodec * const decoder = avcodec_find_decoder (cp->codec_id);
if (decoder == NULL) {
softfail ("find_decoder");
}
@@ -282,11 +282,13 @@ static bool openFilter (player_t * const player) {
/* abuffer */
AVRational time_base = player->st->time_base;
+ char channelLayout[128];
+ av_channel_layout_describe(&player->cctx->ch_layout, channelLayout, sizeof(channelLayout));
snprintf (strbuf, sizeof (strbuf),
- "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=0x%"PRIx64,
+ "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=%s",
time_base.num, time_base.den, cp->sample_rate,
av_get_sample_fmt_name (player->cctx->sample_fmt),
- cp->channel_layout);
+ channelLayout);
if ((ret = avfilter_graph_create_filter (&player->fabuf,
avfilter_get_by_name ("abuffer"), "source", strbuf, NULL,
player->fgraph)) < 0) {
@@ -340,7 +342,7 @@ static bool openDevice (player_t * const player) {
memset (&aoFmt, 0, sizeof (aoFmt));
aoFmt.bits = av_get_bytes_per_sample (avformat) * 8;
assert (aoFmt.bits > 0);
- aoFmt.channels = cp->channels;
+ aoFmt.channels = cp->ch_layout.nb_channels;
aoFmt.rate = getSampleRate (player);
aoFmt.byte_format = AO_FMT_NATIVE;
@@ -431,8 +433,12 @@ static int play (player_t * const player) {
} else if (ret < 0) {
/* error, abort */
/* mark the EOF, so that BarAoPlayThread can quit*/
- debugPrint (DEBUG_AUDIO, "av_read_frame failed with code %i, sending "
- "NULL frame\n", ret);
+ char error[AV_ERROR_MAX_STRING_SIZE];
+ if (av_strerror(ret, error, sizeof(error)) < 0) {
+ strncpy (error, "(unknown)", sizeof(error)-1);
+ }
+ debugPrint (DEBUG_AUDIO, "av_read_frame failed with code %i (%s), "
+ "sending NULL frame\n", ret, error);
pthread_mutex_lock (&player->aoplayLock);
const int rt = av_buffersrc_add_frame (player->fabuf, NULL);
assert (rt == 0);
@@ -533,7 +539,9 @@ void *BarPlayerThread (void *data) {
if (openFilter (player) && openDevice (player)) {
changeMode (player, PLAYER_PLAYING);
BarPlayerSetVolume (player);
- retry = play (player) == AVERROR_INVALIDDATA &&
+ const int ret = play (player);
+ retry = (ret == AVERROR_INVALIDDATA ||
+ ret == -ECONNRESET) &&
!player->interrupted;
} else {
/* filter missing or audio device busy */
@@ -583,8 +591,7 @@ void *BarAoPlayThread (void *data) {
}
pthread_mutex_unlock (&player->aoplayLock);
- const int numChannels = av_get_channel_layout_nb_channels (
- filteredFrame->channel_layout);
+ const int numChannels = filteredFrame->ch_layout.nb_channels;
const int bps = av_get_bytes_per_sample (filteredFrame->format);
ao_play (player->aoDev, (char *) filteredFrame->data[0],
filteredFrame->nb_samples * numChannels * bps);
diff --git a/src/player.h b/src/player.h
index 3179785..2e44aed 100644
--- a/src/player.h
+++ b/src/player.h
@@ -34,6 +34,7 @@ THE SOFTWARE.
#include <ao/ao.h>
#include <libavformat/avformat.h>
#include <libavfilter/avfilter.h>
+#include <libavcodec/avcodec.h>
#include <piano.h>
#include "settings.h"
diff --git a/src/ui.c b/src/ui.c
index 1ab54ae..0c7386a 100644
--- a/src/ui.c
+++ b/src/ui.c
@@ -161,8 +161,8 @@ static size_t httpFetchCb (char *ptr, size_t size, size_t nmemb,
/* libcurl progress callback. aborts the current request if user pressed ^C
*/
-int progressCb (void * const data, double dltotal, double dlnow,
- double ultotal, double ulnow) {
+int progressCb (void * const data, curl_off_t dltotal, curl_off_t dlnow,
+ curl_off_t ultotal, curl_off_t ulnow) {
const sig_atomic_t lint = *((sig_atomic_t *) data);
if (lint) {
return 1;
@@ -171,6 +171,27 @@ int progressCb (void * const data, double dltotal, double dlnow,
}
}
+/* Error codes from libcurl, which may be temporary and should be retried.
+ */
+static bool temporaryCurlError (const CURLcode code) {
+ switch (code) {
+ case CURLE_COULDNT_RESOLVE_PROXY:
+ case CURLE_COULDNT_RESOLVE_HOST:
+ case CURLE_COULDNT_CONNECT:
+ case CURLE_WEIRD_SERVER_REPLY:
+ case CURLE_READ_ERROR:
+ case CURLE_OPERATION_TIMEDOUT:
+ case CURLE_SSL_CONNECT_ERROR:
+ case CURLE_GOT_NOTHING:
+ case CURLE_SEND_ERROR:
+ case CURLE_RECV_ERROR:
+ return true;
+
+ default:
+ return false;
+ }
+}
+
#define setAndCheck(k,v) \
httpret = curl_easy_setopt (http, k, v); \
assert (httpret == CURLE_OK);
@@ -203,8 +224,8 @@ static CURLcode BarPianoHttpRequest (CURL * const http,
setAndCheck (CURLOPT_POSTFIELDS, req->postData);
setAndCheck (CURLOPT_WRITEFUNCTION, httpFetchCb);
setAndCheck (CURLOPT_WRITEDATA, &buffer);
- setAndCheck (CURLOPT_PROGRESSFUNCTION, progressCb);
- setAndCheck (CURLOPT_PROGRESSDATA, &lint);
+ setAndCheck (CURLOPT_XFERINFOFUNCTION, progressCb);
+ setAndCheck (CURLOPT_XFERINFODATA, &lint);
setAndCheck (CURLOPT_NOPROGRESS, 0);
setAndCheck (CURLOPT_POST, 1);
setAndCheck (CURLOPT_TIMEOUT, settings->timeout);
@@ -248,7 +269,7 @@ static CURLcode BarPianoHttpRequest (CURL * const http,
do {
httpret = curl_easy_perform (http);
++retry;
- if (httpret == CURLE_OPERATION_TIMEDOUT) {
+ if (temporaryCurlError (httpret)) {
free (buffer.data);
buffer.data = NULL;
buffer.pos = 0;
@@ -823,6 +844,49 @@ size_t BarUiListSongs (const BarApp_t * const app,
return i;
}
+enum {
+ NO_DURATION = 0,
+};
+#define NO_POSTFIX ""
+
+/* Print song information to the eventcmd stream
+ * @param Event command stream.
+ * @param Song information.
+ * @param Printed key name postfix, use NO_POSTFIX to print bare keys.
+ * @param Override song length from song parameter, use NO_DURATION if unavailable.
+ */
+static void BarUiEventcmdPrintSong (FILE * restrict stream,
+ const PianoSong_t * const song, const char * const postfix,
+ const unsigned int songDuration) {
+ assert (song != NULL);
+ assert (stream != NULL);
+ assert (postfix != NULL);
+
+ fprintf (stream,
+ "artist%s=%s\n"
+ "title%s=%s\n"
+ "album%s=%s\n"
+ "coverArt%s=%s\n"
+ "rating%s=%i\n"
+ "detailUrl%s=%s\n"
+ "songDuration%s=%u\n",
+ postfix,
+ song->artist,
+ postfix,
+ song->title,
+ postfix,
+ song->album,
+ postfix,
+ song->coverArt,
+ postfix,
+ song->rating,
+ postfix,
+ song->detailUrl,
+ postfix,
+ songDuration == NO_DURATION ? song->length : songDuration
+ );
+}
+
/* Excute external event handler
* @param settings containing the cmdline
* @param event type
@@ -879,36 +943,37 @@ void BarUiStartEventCmd (const BarSettings_t *settings, const char *type,
pthread_mutex_unlock (&player->lock);
fprintf (pipeWriteFd,
- "artist=%s\n"
- "title=%s\n"
- "album=%s\n"
- "coverArt=%s\n"
"stationName=%s\n"
"songStationName=%s\n"
"pRet=%i\n"
"pRetStr=%s\n"
"wRet=%i\n"
"wRetStr=%s\n"
- "songDuration=%u\n"
- "songPlayed=%u\n"
- "rating=%i\n"
- "detailUrl=%s\n",
- curSong == NULL ? "" : curSong->artist,
- curSong == NULL ? "" : curSong->title,
- curSong == NULL ? "" : curSong->album,
- curSong == NULL ? "" : curSong->coverArt,
+ "songPlayed=%u\n",
curStation == NULL ? "" : curStation->name,
songStation == NULL ? "" : songStation->name,
pRet,
PianoErrorToStr (pRet),
wRet,
curl_easy_strerror (wRet),
- songDuration,
- songPlayed,
- curSong == NULL ? PIANO_RATE_NONE : curSong->rating,
- curSong == NULL ? "" : curSong->detailUrl
+ songPlayed
);
+ if (curSong != NULL) {
+ BarUiEventcmdPrintSong (pipeWriteFd, curSong, NO_POSTFIX, songDuration);
+ }
+
+ const PianoSong_t *nextSong = PianoListNextP (curSong);
+ if (nextSong != NULL) {
+ unsigned int i = 0;
+ PianoListForeachP (nextSong) {
+ char postfix[16];
+ snprintf (postfix, sizeof(postfix)-1, "Next%i", i);
+ BarUiEventcmdPrintSong (pipeWriteFd, nextSong, postfix, NO_DURATION);
+ i++;
+ }
+ }
+
if (stations != NULL) {
/* send station list */
PianoStation_t **sortedStations = NULL;
diff --git a/src/ui_act.c b/src/ui_act.c
index ace50ce..fa5c43b 100644
--- a/src/ui_act.c
+++ b/src/ui_act.c
@@ -271,8 +271,12 @@ BarUiActCallback(BarUiActExplain) {
BarUiMsg (&app->settings, MSG_INFO, "Receiving explanation... ");
if (BarUiActDefaultPianoCall (PIANO_REQUEST_EXPLAIN, &reqData)) {
- BarUiMsg (&app->settings, MSG_INFO, "%s\n", reqData.retExplain);
- free (reqData.retExplain);
+ if (reqData.retExplain == NULL) {
+ BarUiMsg (&app->settings, MSG_ERR, "No explanation provided.\n");
+ } else {
+ BarUiMsg (&app->settings, MSG_INFO, "%s\n", reqData.retExplain);
+ free (reqData.retExplain);
+ }
}
BarUiActDefaultEventcmd ("songexplain");
}
@@ -785,7 +789,12 @@ BarUiActCallback(BarUiActManageStation) {
}
/* enable submenus depending on data availability */
- strcpy (question, "Delete ");
+ if (reqData.info.artistSeeds != NULL ||
+ reqData.info.songSeeds != NULL ||
+ reqData.info.stationSeeds != NULL ||
+ reqData.info.feedback != NULL) {
+ strcpy (question, "Delete ");
+ }
if (reqData.info.artistSeeds != NULL) {
strcat (question, "[a]rtist");
*allowedPos++ = 'a';