linux-kernel.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
From: Samuel Holland <samuel@sholland.org>
To: Mark Brown <broonie@kernel.org>,
	Liam Girdwood <lgirdwood@gmail.com>,
	Maxime Ripard <mripard@kernel.org>, Chen-Yu Tsai <wens@csie.org>,
	Jaroslav Kysela <perex@perex.cz>, Takashi Iwai <tiwai@suse.com>
Cc: Ondrej Jirman <megous@megous.com>,
	alsa-devel@alsa-project.org,
	linux-arm-kernel@lists.infradead.org,
	linux-kernel@vger.kernel.org,
	Samuel Holland <samuel@sholland.org>
Subject: [PATCH 18/25] ASoC: sun8i-codec: Automatically set the system sample rate
Date: Wed, 30 Sep 2020 21:11:41 -0500	[thread overview]
Message-ID: <20201001021148.15852-19-samuel@sholland.org> (raw)
In-Reply-To: <20201001021148.15852-1-samuel@sholland.org>

The sun8i codec has three clock/sample rate domains:
 - The AIF1 domain, with a sample rate equal to AIF1 LRCK
 - The AIF2 domain, with a sample rate equal to AIF2 LRCK
 - The SYSCLK domain, containing the ADC, DAC, and effects (AGC/DRC),
   with a sample rate given by a divisor from SYSCLK. The divisor is
   controlled by the AIF1_FS or AIF2_FS field in SYS_SR_CTRL, depending
   on if SYSCLK's source is AIF1CLK or AIF2CLK, respectively. The exact
   sample rate depends on if SYSCLK is running at 22.6 MHz or 24.6 MHz.

When an AIF (currently only AIF1) is active, the ADC and DAC should run
at that sample rate to avoid artifacting. Sample rate conversion is only
available when multiple AIFs are active and are routed to each other;
this means the sample rate conversion hardware usually cannot be used.

Only attach the event hook to the channel 0 AIF widgets, since we only
need one event when a DAI stream starts or stops. Channel 0 is always
brought up with a DAI stream, regardless of the number of channels in
the stream.

The ADC and DAC (along with their effects blocks) can be used even if
no AIFs are in use. In that case, we should select an appropriate sample
rate divisor, instead of keeping the last-used AIF sample rate.
44.1/48 kHz was chosen to balance audio quality and power consumption.

Since the sample rate is tied to active AIF paths, disabling pmdown_time
allows switching to the optimal sample rate immediately, instead of
after a 5 second delay.

Signed-off-by: Samuel Holland <samuel@sholland.org>
---
 sound/soc/sunxi/sun8i-codec.c | 103 +++++++++++++++++++++++++++-------
 1 file changed, 84 insertions(+), 19 deletions(-)

diff --git a/sound/soc/sunxi/sun8i-codec.c b/sound/soc/sunxi/sun8i-codec.c
index f4b2a7cc9810..43b4319e3d84 100644
--- a/sound/soc/sunxi/sun8i-codec.c
+++ b/sound/soc/sunxi/sun8i-codec.c
@@ -89,31 +89,36 @@
 #define SUN8I_SYS_SR_CTRL_AIF1_FS_MASK		GENMASK(15, 12)
 #define SUN8I_SYS_SR_CTRL_AIF2_FS_MASK		GENMASK(11, 8)
 #define SUN8I_AIF1CLK_CTRL_AIF1_CLK_INV_MASK	GENMASK(14, 13)
 #define SUN8I_AIF1CLK_CTRL_AIF1_BCLK_DIV_MASK	GENMASK(12, 9)
 #define SUN8I_AIF1CLK_CTRL_AIF1_LRCK_DIV_MASK	GENMASK(8, 6)
 #define SUN8I_AIF1CLK_CTRL_AIF1_WORD_SIZ_MASK	GENMASK(5, 4)
 #define SUN8I_AIF1CLK_CTRL_AIF1_DATA_FMT_MASK	GENMASK(3, 2)
 
+#define SUN8I_CODEC_PASSTHROUGH_SAMPLE_RATE 48000
+
 #define SUN8I_CODEC_PCM_RATES	(SNDRV_PCM_RATE_8000_48000|\
 				 SNDRV_PCM_RATE_88200     |\
 				 SNDRV_PCM_RATE_96000     |\
 				 SNDRV_PCM_RATE_176400    |\
 				 SNDRV_PCM_RATE_192000    |\
 				 SNDRV_PCM_RATE_KNOT)
 
 enum {
 	AIF1,
 	NAIFS
 };
 
 struct sun8i_codec_aif {
+	unsigned int	sample_rate;
 	unsigned int	slots;
 	unsigned int	slot_width;
+	unsigned int	active_streams	: 2;
+	unsigned int	open_streams	: 2;
 };
 
 struct sun8i_codec_quirks {
 	bool legacy_widgets	: 1;
 	bool lrck_inversion	: 1;
 };
 
 struct sun8i_codec {
@@ -144,21 +149,19 @@ static int sun8i_codec_runtime_suspend(struct device *dev)
 	struct sun8i_codec *scodec = dev_get_drvdata(dev);
 
 	regcache_cache_only(scodec->regmap, true);
 	regcache_mark_dirty(scodec->regmap);
 
 	return 0;
 }
 
-static int sun8i_codec_get_hw_rate(struct snd_pcm_hw_params *params)
+static int sun8i_codec_get_hw_rate(unsigned int sample_rate)
 {
-	unsigned int rate = params_rate(params);
-
-	switch (rate) {
+	switch (sample_rate) {
 	case 7350:
 	case 8000:
 		return 0x0;
 	case 11025:
 		return 0x1;
 	case 12000:
 		return 0x2;
 	case 14700:
@@ -181,16 +184,43 @@ static int sun8i_codec_get_hw_rate(struct snd_pcm_hw_params *params)
 	case 176400:
 	case 192000:
 		return 0xa;
 	default:
 		return -EINVAL;
 	}
 }
 
+static int sun8i_codec_update_sample_rate(struct sun8i_codec *scodec)
+{
+	unsigned int max_rate = 0;
+	int hw_rate, i;
+
+	for (i = AIF1; i < NAIFS; ++i) {
+		struct sun8i_codec_aif *aif = &scodec->aifs[i];
+
+		if (aif->active_streams)
+			max_rate = max(max_rate, aif->sample_rate);
+	}
+
+	/* Set the sample rate for ADC->DAC passthrough when no AIF is active. */
+	if (!max_rate)
+		max_rate = SUN8I_CODEC_PASSTHROUGH_SAMPLE_RATE;
+
+	hw_rate = sun8i_codec_get_hw_rate(max_rate);
+	if (hw_rate < 0)
+		return hw_rate;
+
+	regmap_update_bits(scodec->regmap, SUN8I_SYS_SR_CTRL,
+			   SUN8I_SYS_SR_CTRL_AIF1_FS_MASK,
+			   hw_rate << SUN8I_SYS_SR_CTRL_AIF1_FS);
+
+	return 0;
+}
+
 static int sun8i_codec_set_fmt(struct snd_soc_dai *dai, unsigned int fmt)
 {
 	struct sun8i_codec *scodec = snd_soc_dai_get_drvdata(dai);
 	u32 format, invert, value;
 
 	/* clock masters */
 	switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) {
 	case SND_SOC_DAIFMT_CBS_CFS: /* Codec slave, DAI master */
@@ -350,19 +380,20 @@ static int sun8i_codec_get_lrck_div_order(unsigned int slots,
 }
 
 static int sun8i_codec_hw_params(struct snd_pcm_substream *substream,
 				 struct snd_pcm_hw_params *params,
 				 struct snd_soc_dai *dai)
 {
 	struct sun8i_codec *scodec = snd_soc_dai_get_drvdata(dai);
 	struct sun8i_codec_aif *aif = &scodec->aifs[dai->id];
+	unsigned int sample_rate = params_rate(params);
 	unsigned int slots = aif->slots ?: params_channels(params);
 	unsigned int slot_width = aif->slot_width ?: params_width(params);
-	int lrck_div_order, sample_rate, word_size;
+	int lrck_div_order, word_size;
 	u8 bclk_div;
 
 	/* word size */
 	switch (params_width(params)) {
 	case 8:
 		word_size = 0x0;
 		break;
 	case 16:
@@ -387,36 +418,48 @@ static int sun8i_codec_hw_params(struct snd_pcm_substream *substream,
 	if (lrck_div_order < 0)
 		return lrck_div_order;
 
 	regmap_update_bits(scodec->regmap, SUN8I_AIF1CLK_CTRL,
 			   SUN8I_AIF1CLK_CTRL_AIF1_LRCK_DIV_MASK,
 			   (lrck_div_order - 4) << SUN8I_AIF1CLK_CTRL_AIF1_LRCK_DIV);
 
 	/* BCLK divider (SYSCLK/BCLK ratio) */
-	bclk_div = sun8i_codec_get_bclk_div(scodec, lrck_div_order, params_rate(params));
+	bclk_div = sun8i_codec_get_bclk_div(scodec, lrck_div_order, sample_rate);
 	regmap_update_bits(scodec->regmap, SUN8I_AIF1CLK_CTRL,
 			   SUN8I_AIF1CLK_CTRL_AIF1_BCLK_DIV_MASK,
 			   bclk_div << SUN8I_AIF1CLK_CTRL_AIF1_BCLK_DIV);
 
-	sample_rate = sun8i_codec_get_hw_rate(params);
-	if (sample_rate < 0)
-		return sample_rate;
+	aif->sample_rate = sample_rate;
+	aif->open_streams |= BIT(substream->stream);
 
-	regmap_update_bits(scodec->regmap, SUN8I_SYS_SR_CTRL,
-			   SUN8I_SYS_SR_CTRL_AIF1_FS_MASK,
-			   sample_rate << SUN8I_SYS_SR_CTRL_AIF1_FS);
+	return sun8i_codec_update_sample_rate(scodec);
+}
+
+static int sun8i_codec_hw_free(struct snd_pcm_substream *substream,
+			       struct snd_soc_dai *dai)
+{
+	struct sun8i_codec *scodec = snd_soc_dai_get_drvdata(dai);
+	struct sun8i_codec_aif *aif = &scodec->aifs[dai->id];
+
+	if (aif->open_streams != BIT(substream->stream))
+		goto done;
 
+	aif->sample_rate = 0;
+
+done:
+	aif->open_streams &= ~BIT(substream->stream);
 	return 0;
 }
 
 static const struct snd_soc_dai_ops sun8i_codec_dai_ops = {
 	.set_fmt	= sun8i_codec_set_fmt,
 	.set_tdm_slot	= sun8i_codec_set_tdm_slot,
 	.hw_params	= sun8i_codec_hw_params,
+	.hw_free	= sun8i_codec_hw_free,
 };
 
 static struct snd_soc_dai_driver sun8i_codec_dais[] = {
 	{
 		.name	= "sun8i-codec-aif1",
 		.id	= AIF1,
 		.ops	= &sun8i_codec_dai_ops,
 		/* capture capabilities */
@@ -437,16 +480,32 @@ static struct snd_soc_dai_driver sun8i_codec_dais[] = {
 			.formats	= SNDRV_PCM_FMTBIT_S16_LE,
 		},
 		.symmetric_rates	= true,
 		.symmetric_channels	= true,
 		.symmetric_samplebits	= true,
 	},
 };
 
+static int sun8i_codec_aif_event(struct snd_soc_dapm_widget *w,
+				 struct snd_kcontrol *kcontrol, int event)
+{
+	struct snd_soc_component *component = snd_soc_dapm_to_component(w->dapm);
+	struct sun8i_codec *scodec = snd_soc_component_get_drvdata(component);
+	struct sun8i_codec_aif *aif = &scodec->aifs[w->sname[3] - '1'];
+	int stream = w->id == snd_soc_dapm_aif_out;
+
+	if (SND_SOC_DAPM_EVENT_ON(event))
+		aif->active_streams |= BIT(stream);
+	else
+		aif->active_streams &= ~BIT(stream);
+
+	return sun8i_codec_update_sample_rate(scodec);
+}
+
 static const char *const sun8i_aif_stereo_mux_enum_values[] = {
 	"Stereo", "Reverse Stereo", "Sum Mono", "Mix Mono"
 };
 
 static SOC_ENUM_DOUBLE_DECL(sun8i_aif1_ad0_stereo_mux_enum,
 			    SUN8I_AIF1_ADCDAT_CTRL,
 			    SUN8I_AIF1_ADCDAT_CTRL_AIF1_AD0L_SRC,
 			    SUN8I_AIF1_ADCDAT_CTRL_AIF1_AD0R_SRC,
@@ -539,19 +598,21 @@ static const struct snd_soc_dapm_widget sun8i_codec_dapm_widgets[] = {
 	SND_SOC_DAPM_SUPPLY("ADC",
 			    SUN8I_ADC_DIG_CTRL,
 			    SUN8I_ADC_DIG_CTRL_ENAD, 0, NULL, 0),
 	SND_SOC_DAPM_SUPPLY("DAC",
 			    SUN8I_DAC_DIG_CTRL,
 			    SUN8I_DAC_DIG_CTRL_ENDA, 0, NULL, 0),
 
 	/* AIF "ADC" Outputs */
-	SND_SOC_DAPM_AIF_OUT("AIF1 AD0L", "AIF1 Capture", 0,
-			     SUN8I_AIF1_ADCDAT_CTRL,
-			     SUN8I_AIF1_ADCDAT_CTRL_AIF1_AD0L_ENA, 0),
+	SND_SOC_DAPM_AIF_OUT_E("AIF1 AD0L", "AIF1 Capture", 0,
+			       SUN8I_AIF1_ADCDAT_CTRL,
+			       SUN8I_AIF1_ADCDAT_CTRL_AIF1_AD0L_ENA, 0,
+			       sun8i_codec_aif_event,
+			       SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD),
 	SND_SOC_DAPM_AIF_OUT("AIF1 AD0R", "AIF1 Capture", 1,
 			     SUN8I_AIF1_ADCDAT_CTRL,
 			     SUN8I_AIF1_ADCDAT_CTRL_AIF1_AD0R_ENA, 0),
 
 	/* AIF "ADC" Mono/Stereo Muxes */
 	SND_SOC_DAPM_MUX("AIF1 AD0L Stereo Mux", SND_SOC_NOPM, 0, 0,
 			 &sun8i_aif1_ad0_stereo_mux_control),
 	SND_SOC_DAPM_MUX("AIF1 AD0R Stereo Mux", SND_SOC_NOPM, 0, 0,
@@ -565,19 +626,21 @@ static const struct snd_soc_dapm_widget sun8i_codec_dapm_widgets[] = {
 
 	/* AIF "DAC" Mono/Stereo Muxes */
 	SND_SOC_DAPM_MUX("AIF1 DA0L Stereo Mux", SND_SOC_NOPM, 0, 0,
 			 &sun8i_aif1_da0_stereo_mux_control),
 	SND_SOC_DAPM_MUX("AIF1 DA0R Stereo Mux", SND_SOC_NOPM, 0, 0,
 			 &sun8i_aif1_da0_stereo_mux_control),
 
 	/* AIF "DAC" Inputs */
-	SND_SOC_DAPM_AIF_IN("AIF1 DA0L", "AIF1 Playback", 0,
-			    SUN8I_AIF1_DACDAT_CTRL,
-			    SUN8I_AIF1_DACDAT_CTRL_AIF1_DA0L_ENA, 0),
+	SND_SOC_DAPM_AIF_IN_E("AIF1 DA0L", "AIF1 Playback", 0,
+			      SUN8I_AIF1_DACDAT_CTRL,
+			      SUN8I_AIF1_DACDAT_CTRL_AIF1_DA0L_ENA, 0,
+			      sun8i_codec_aif_event,
+			      SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD),
 	SND_SOC_DAPM_AIF_IN("AIF1 DA0R", "AIF1 Playback", 1,
 			    SUN8I_AIF1_DACDAT_CTRL,
 			    SUN8I_AIF1_DACDAT_CTRL_AIF1_DA0R_ENA, 0),
 
 	/* ADC Inputs (connected to analog codec DAPM context) */
 	SND_SOC_DAPM_ADC("ADCL", NULL, SND_SOC_NOPM, 0, 0),
 	SND_SOC_DAPM_ADC("ADCR", NULL, SND_SOC_NOPM, 0, 0),
 
@@ -722,27 +785,29 @@ static int sun8i_codec_component_probe(struct snd_soc_component *component)
 			   SUN8I_SYSCLK_CTL_AIF1CLK_SRC_PLL |
 			   SUN8I_SYSCLK_CTL_AIF2CLK_SRC_PLL);
 
 	/* Use AIF1CLK as the SYSCLK parent since AIF1 is used most often. */
 	regmap_update_bits(scodec->regmap, SUN8I_SYSCLK_CTL,
 			   BIT(SUN8I_SYSCLK_CTL_SYSCLK_SRC),
 			   SUN8I_SYSCLK_CTL_SYSCLK_SRC_AIF1CLK);
 
+	/* Program the default sample rate. */
+	sun8i_codec_update_sample_rate(scodec);
+
 	return 0;
 }
 
 static const struct snd_soc_component_driver sun8i_soc_component = {
 	.dapm_widgets		= sun8i_codec_dapm_widgets,
 	.num_dapm_widgets	= ARRAY_SIZE(sun8i_codec_dapm_widgets),
 	.dapm_routes		= sun8i_codec_dapm_routes,
 	.num_dapm_routes	= ARRAY_SIZE(sun8i_codec_dapm_routes),
 	.probe			= sun8i_codec_component_probe,
 	.idle_bias_on		= 1,
-	.use_pmdown_time	= 1,
 	.endianness		= 1,
 	.non_legacy_dai_naming	= 1,
 };
 
 static const struct regmap_config sun8i_codec_regmap_config = {
 	.reg_bits	= 32,
 	.reg_stride	= 4,
 	.val_bits	= 32,
-- 
2.26.2


  parent reply	other threads:[~2020-10-01  2:15 UTC|newest]

Thread overview: 60+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2020-10-01  2:11 [PATCH 00/25] ASoC: sun8i-codec: support for AIF2 and AIF3 Samuel Holland
2020-10-01  2:11 ` [PATCH 01/25] ASoC: sun8i-codec: Set up clock tree at probe time Samuel Holland
2020-10-05  9:53   ` Maxime Ripard
2020-10-01  2:11 ` [PATCH 02/25] ASoC: sun8i-codec: Swap module clock/reset dependencies Samuel Holland
2020-10-05  9:54   ` Maxime Ripard
2020-10-01  2:11 ` [PATCH 03/25] ASoC: sun8i-codec: Sort DAPM controls, widgets, and routes Samuel Holland
2020-10-05  9:54   ` Maxime Ripard
2020-10-01  2:11 ` [PATCH 04/25] ASoC: sun8i-codec: Consistently name DAPM widgets " Samuel Holland
2020-10-05  9:57   ` Maxime Ripard
2020-10-01  2:11 ` [PATCH 05/25] ASoC: sun8i-codec: Correct DAPM widget types Samuel Holland
2020-10-05  9:57   ` Maxime Ripard
2020-10-01  2:11 ` [PATCH 06/25] ASoC: sun8i-codec: Fix AIF widget channel references Samuel Holland
2020-10-05 10:00   ` Maxime Ripard
2020-10-01  2:11 ` [PATCH 07/25] ASoC: sun8i-codec: Enable AIF mono/stereo control Samuel Holland
2020-10-05 10:00   ` Maxime Ripard
2020-10-01  2:11 ` [PATCH 08/25] ASoC: sun8i-codec: Use snd_soc_dai_get_drvdata Samuel Holland
2020-10-05 10:01   ` Maxime Ripard
2020-10-01  2:11 ` [PATCH 09/25] ASoC: sun8i-codec: Prepare to extend the DAI driver Samuel Holland
2020-10-05 11:24   ` Maxime Ripard
2020-10-01  2:11 ` [PATCH 10/25] ASoC: sun8i-codec: Program format before clock inversion Samuel Holland
2020-10-05 11:26   ` Maxime Ripard
2020-10-01  2:11 ` [PATCH 11/25] ASoC: sun8i-codec: Enable all supported clock inversions Samuel Holland
2020-10-05 11:30   ` Maxime Ripard
2020-10-06  4:29     ` Samuel Holland
2020-10-08 12:59       ` Maxime Ripard
2020-10-01  2:11 ` [PATCH 12/25] ASoC: sun8i-codec: Program the correct word size Samuel Holland
2020-10-05 11:30   ` Maxime Ripard
2020-10-01  2:11 ` [PATCH 13/25] ASoC: sun8i-codec: Round up the LRCK divisor Samuel Holland
2020-10-05 11:31   ` Maxime Ripard
2020-10-01  2:11 ` [PATCH 14/25] ASoC: sun8i-codec: Correct the BCLK divisor calculation Samuel Holland
2020-10-05 11:34   ` Maxime Ripard
2020-10-01  2:11 ` [PATCH 15/25] ASoC: sun8i-codec: Support the TDM slot binding Samuel Holland
2020-10-05 11:34   ` Maxime Ripard
2020-10-01  2:11 ` [PATCH 16/25] ASoC: sun8i-codec: Enforce symmetric DAI parameters Samuel Holland
2020-10-05 11:39   ` Maxime Ripard
2020-10-01  2:11 ` [PATCH 17/25] ASoC: sun8i-codec: Enable all supported sample rates Samuel Holland
2020-10-05 11:39   ` Maxime Ripard
2020-10-01  2:11 ` Samuel Holland [this message]
2020-10-05 11:58   ` [PATCH 18/25] ASoC: sun8i-codec: Automatically set the system sample rate Maxime Ripard
2020-10-01  2:11 ` [PATCH 19/25] ASoC: sun8i-codec: Constrain to compatible sample rates Samuel Holland
2020-10-05 11:59   ` Maxime Ripard
2020-10-01  2:11 ` [PATCH 20/25] ASoC: sun8i-codec: Protect the clock rate while streams are open Samuel Holland
2020-10-01 13:27   ` kernel test robot
2020-10-05 12:01   ` Maxime Ripard
2020-10-05 13:15     ` Chen-Yu Tsai
2020-10-06  4:46       ` Samuel Holland
2020-10-06  4:43     ` Samuel Holland
2020-10-08 13:02       ` Maxime Ripard
2020-10-01  2:11 ` [PATCH 21/25] ASoC: sun8i-codec: Require an exact BCLK divisor match Samuel Holland
2020-10-05 12:01   ` Maxime Ripard
2020-10-01  2:11 ` [PATCH 22/25] ASoC: sun8i-codec: Enable all supported PCM formats Samuel Holland
2020-10-05 12:02   ` Maxime Ripard
2020-10-01  2:11 ` [PATCH 23/25] ASoC: sun8i-codec: Generalize AIF clock control Samuel Holland
2020-10-05 12:04   ` Maxime Ripard
2020-10-06  4:51     ` Samuel Holland
2020-10-12 11:17       ` Maxime Ripard
2020-10-01  2:11 ` [PATCH 24/25] ASoC: sun8i-codec: Add a DAI, widgets, and routes for AIF2 Samuel Holland
2020-10-05 12:05   ` Maxime Ripard
2020-10-01  2:11 ` [PATCH 25/25] ASoC: sun8i-codec: Add a DAI, widgets, and routes for AIF3 Samuel Holland
2020-10-05 17:54 ` [PATCH 00/25] ASoC: sun8i-codec: support for AIF2 and AIF3 Mark Brown

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20201001021148.15852-19-samuel@sholland.org \
    --to=samuel@sholland.org \
    --cc=alsa-devel@alsa-project.org \
    --cc=broonie@kernel.org \
    --cc=lgirdwood@gmail.com \
    --cc=linux-arm-kernel@lists.infradead.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=megous@megous.com \
    --cc=mripard@kernel.org \
    --cc=perex@perex.cz \
    --cc=tiwai@suse.com \
    --cc=wens@csie.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).