Add quality and qualityAlpha to avifEncoder

Change the default of minQuantizer and minQuantizerAlpha to
AVIF_QUANTIZER_BEST_QUALITY (0). Change the default of maxQuantizer and
maxQuantizerAlpha to AVIF_QUANTIZER_WORST_QUALITY (63).

The default of quality is based on the average of minQuantizer and
maxQuantizer. Similarly the default of qualityAlpha is based on the
average of minQuantizerAlpha and maxQuantizerAlpha.

Map quality and qualityAlpha in the range of 0..100 linearly to
quantizer values (QP) in the range of 63..0.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f10b3e9..59ba49c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,8 @@
 
 There are incompatible ABI changes in this release. The clli member was added
 to the avifImage struct. The repetitionCount member was added to the avifEncoder
-and avifDecoder structs.
+and avifDecoder structs. The quality and qualityAlpha members were added to the
+avifEncoder struct.
 
 ### Added
 * Add STATIC library target avif_internal to allow tests to access functions
@@ -16,6 +17,11 @@
 * Add clli metadata read and write support
 * Add repetitionCount member to avifEncoder and avifDecoder structs to specify
   the number of repetitions for animated image sequences.
+* Add quality and qualityAlpha to avifEncoder. Note: minQuantizer,
+  maxQuantizer, minQuantizerAlpha, and maxQuantizerAlpha are deprecated. Code
+  should be updated to set quality (and qualityAlpha if applicable) and leave
+  minQuantizer, maxQuantizer, minQuantizerAlpha, and maxQuantizerAlpha
+  initialized to the default values.
 
 ### Changed
 * Exif and XMP metadata is exported to PNG and JPEG files by default,
@@ -26,6 +32,12 @@
 * Update svt.cmd/svt.sh: v1.3.0
 * avifImageCopy() no longer accepts source U and V channels to be NULL for
   non-4:0:0 input if Y is not NULL and if AVIF_PLANES_YUV is specified.
+* The default values of the maxQuantizer and maxQuantizerAlpha members of
+  avifEncoder changed from AVIF_QUANTIZER_LOSSLESS (0) to
+  AVIF_QUANTIZER_WORST_QUALITY (63). The behavior changed if minQuantizer and
+  maxQuantizer are left initialized to the default values. Code should be
+  updated to set the quality member. Similarly for the alpha quantizers and
+  qualityAlpha.
 
 ## [0.11.1] - 2022-10-19
 
diff --git a/apps/avifenc.c b/apps/avifenc.c
index d9c2d8e..339293c 100644
--- a/apps/avifenc.c
+++ b/apps/avifenc.c
@@ -66,22 +66,14 @@
     printf("                                        M = matrix coefficients\n");
     printf("                                        (use 2 for any you wish to leave unspecified)\n");
     printf("    -r,--range RANGE                  : YUV range [limited or l, full or f]. (JPEG/PNG only, default: full; For y4m or stdin, range is retained)\n");
-    printf("    --min Q                           : Set min quantizer for color (%d-%d, where %d is lossless)\n",
-           AVIF_QUANTIZER_BEST_QUALITY,
-           AVIF_QUANTIZER_WORST_QUALITY,
-           AVIF_QUANTIZER_LOSSLESS);
-    printf("    --max Q                           : Set max quantizer for color (%d-%d, where %d is lossless)\n",
-           AVIF_QUANTIZER_BEST_QUALITY,
-           AVIF_QUANTIZER_WORST_QUALITY,
-           AVIF_QUANTIZER_LOSSLESS);
-    printf("    --minalpha Q                      : Set min quantizer for alpha (%d-%d, where %d is lossless)\n",
-           AVIF_QUANTIZER_BEST_QUALITY,
-           AVIF_QUANTIZER_WORST_QUALITY,
-           AVIF_QUANTIZER_LOSSLESS);
-    printf("    --maxalpha Q                      : Set max quantizer for alpha (%d-%d, where %d is lossless)\n",
-           AVIF_QUANTIZER_BEST_QUALITY,
-           AVIF_QUANTIZER_WORST_QUALITY,
-           AVIF_QUANTIZER_LOSSLESS);
+    printf("    -q,--qcolor Q                     : Set quality for color (%d-%d, where %d is lossless)\n",
+           AVIF_QUALITY_WORST,
+           AVIF_QUALITY_BEST,
+           AVIF_QUALITY_LOSSLESS);
+    printf("    --qalpha Q                        : Set quality for alpha (%d-%d, where %d is lossless)\n",
+           AVIF_QUALITY_WORST,
+           AVIF_QUALITY_BEST,
+           AVIF_QUALITY_LOSSLESS);
     printf("    --tilerowslog2 R                  : Set log2 of number of tile rows (0-6, default: 0)\n");
     printf("    --tilecolslog2 C                  : Set log2 of number of tile columns (0-6, default: 0)\n");
     printf("    --autotiling                      : Set --tilerowslog2 and --tilecolslog2 automatically\n");
@@ -109,6 +101,22 @@
     printf("    --irot ANGLE                      : Add irot property (rotation). [0-3], makes (90 * ANGLE) degree rotation anti-clockwise\n");
     printf("    --imir MODE                       : Add imir property (mirroring). 0=top-to-bottom, 1=left-to-right\n");
     printf("    --repetition-count N or infinite  : Number of times an animated image sequence will be repeated. Use 'infinite' for infinite repetitions (Default: infinite)\n");
+    printf("    --min QP                          : Set min quantizer for color (%d-%d, where %d is lossless)\n",
+           AVIF_QUANTIZER_BEST_QUALITY,
+           AVIF_QUANTIZER_WORST_QUALITY,
+           AVIF_QUANTIZER_LOSSLESS);
+    printf("    --max QP                          : Set max quantizer for color (%d-%d, where %d is lossless)\n",
+           AVIF_QUANTIZER_BEST_QUALITY,
+           AVIF_QUANTIZER_WORST_QUALITY,
+           AVIF_QUANTIZER_LOSSLESS);
+    printf("    --minalpha QP                     : Set min quantizer for alpha (%d-%d, where %d is lossless)\n",
+           AVIF_QUANTIZER_BEST_QUALITY,
+           AVIF_QUANTIZER_WORST_QUALITY,
+           AVIF_QUANTIZER_LOSSLESS);
+    printf("    --maxalpha QP                     : Set max quantizer for alpha (%d-%d, where %d is lossless)\n",
+           AVIF_QUANTIZER_BEST_QUALITY,
+           AVIF_QUANTIZER_WORST_QUALITY,
+           AVIF_QUANTIZER_LOSSLESS);
     printf("    --                                : Signals the end of options. Everything after this is interpreted as file names.\n");
     printf("\n");
     if (avifCodecName(AVIF_CODEC_CHOICE_AOM, 0)) {
@@ -137,18 +145,18 @@
 }
 
 // This is *very* arbitrary, I just want to set people's expectations a bit
-static const char * quantizerString(int quantizer)
+static const char * qualityString(int quality)
 {
-    if (quantizer == 0) {
+    if (quality == AVIF_QUALITY_LOSSLESS) {
         return "Lossless";
     }
-    if (quantizer <= 12) {
+    if (quality >= 80) {
         return "High";
     }
-    if (quantizer <= 32) {
+    if (quality >= 50) {
         return "Medium";
     }
-    if (quantizer == AVIF_QUANTIZER_WORST_QUALITY) {
+    if (quality == AVIF_QUALITY_WORST) {
         return "Worst";
     }
     return "Low";
@@ -428,6 +436,10 @@
     return AVIF_TRUE;
 }
 
+#define INVALID_QUALITY -1
+#define DEFAULT_QUALITY 60 // Maps to a quantizer (QP) of 25.
+#define DEFAULT_QUALITY_ALPHA AVIF_QUALITY_LOSSLESS
+
 int main(int argc, char * argv[])
 {
     if (argc < 2) {
@@ -447,6 +459,8 @@
 
     int returnCode = 0;
     int jobs = 1;
+    int quality = INVALID_QUALITY;
+    int qualityAlpha = INVALID_QUALITY;
     int minQuantizer = -1;
     int maxQuantizer = -1;
     int minQuantizerAlpha = -1;
@@ -564,6 +578,24 @@
         } else if (!strcmp(arg, "-k") || !strcmp(arg, "--keyframe")) {
             NEXTARG();
             keyframeInterval = atoi(arg);
+        } else if (!strcmp(arg, "-q") || !strcmp(arg, "--qcolor")) {
+            NEXTARG();
+            quality = atoi(arg);
+            if (quality < AVIF_QUALITY_WORST) {
+                quality = AVIF_QUALITY_WORST;
+            }
+            if (quality > AVIF_QUALITY_BEST) {
+                quality = AVIF_QUALITY_BEST;
+            }
+        } else if (!strcmp(arg, "--qalpha")) {
+            NEXTARG();
+            qualityAlpha = atoi(arg);
+            if (qualityAlpha < AVIF_QUALITY_WORST) {
+                qualityAlpha = AVIF_QUALITY_WORST;
+            }
+            if (qualityAlpha > AVIF_QUALITY_BEST) {
+                qualityAlpha = AVIF_QUALITY_BEST;
+            }
         } else if (!strcmp(arg, "--min")) {
             NEXTARG();
             minQuantizer = atoi(arg);
@@ -841,6 +873,13 @@
         }
         // Don't subsample when using AVIF_MATRIX_COEFFICIENTS_IDENTITY.
         input.requestedFormat = AVIF_PIXEL_FORMAT_YUV444;
+        // Quality.
+        if ((quality != INVALID_QUALITY && quality != AVIF_QUALITY_LOSSLESS) ||
+            (qualityAlpha != INVALID_QUALITY && qualityAlpha != AVIF_QUALITY_LOSSLESS)) {
+            fprintf(stderr, "Quality cannot be set in lossless mode, except to %d.\n", AVIF_QUALITY_LOSSLESS);
+            returnCode = 1;
+        }
+        quality = qualityAlpha = AVIF_QUALITY_LOSSLESS;
         // Quantizers.
         if (minQuantizer > 0 || maxQuantizer > 0 || minQuantizerAlpha > 0 || maxQuantizerAlpha > 0) {
             fprintf(stderr, "Quantizers cannot be set in lossless mode, except to 0.\n");
@@ -873,16 +912,32 @@
     } else {
         // Set lossy defaults.
         if (minQuantizer == -1) {
-            minQuantizer = 24;
-        }
-        if (maxQuantizer == -1) {
-            maxQuantizer = 26;
+            assert(maxQuantizer == -1);
+            if (quality == INVALID_QUALITY) {
+                quality = DEFAULT_QUALITY;
+            }
+            minQuantizer = AVIF_QUANTIZER_BEST_QUALITY;
+            maxQuantizer = AVIF_QUANTIZER_WORST_QUALITY;
+        } else {
+            assert(maxQuantizer != -1);
+            if (quality == INVALID_QUALITY) {
+                const int quantizer = (minQuantizer + maxQuantizer) / 2;
+                quality = ((63 - quantizer) * 100 + 31) / 63;
+            }
         }
         if (minQuantizerAlpha == -1) {
-            minQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS;
-        }
-        if (maxQuantizerAlpha == -1) {
-            maxQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS;
+            assert(maxQuantizerAlpha == -1);
+            if (qualityAlpha == INVALID_QUALITY) {
+                qualityAlpha = DEFAULT_QUALITY_ALPHA;
+            }
+            minQuantizerAlpha = AVIF_QUANTIZER_BEST_QUALITY;
+            maxQuantizerAlpha = AVIF_QUANTIZER_WORST_QUALITY;
+        } else {
+            assert(maxQuantizerAlpha != -1);
+            if (qualityAlpha == INVALID_QUALITY) {
+                const int quantizerAlpha = (minQuantizerAlpha + maxQuantizerAlpha) / 2;
+                qualityAlpha = ((63 - quantizerAlpha) * 100 + 31) / 63;
+            }
         }
     }
 
@@ -1044,8 +1099,8 @@
         usingAOM = AVIF_TRUE;
     }
     avifBool hasAlpha = (image->alphaPlane && image->alphaRowBytes);
-    avifBool losslessColorQP = (minQuantizer == AVIF_QUANTIZER_LOSSLESS) && (maxQuantizer == AVIF_QUANTIZER_LOSSLESS);
-    avifBool losslessAlphaQP = (minQuantizerAlpha == AVIF_QUANTIZER_LOSSLESS) && (maxQuantizerAlpha == AVIF_QUANTIZER_LOSSLESS);
+    avifBool usingLosslessColor = (quality == AVIF_QUALITY_LOSSLESS);
+    avifBool usingLosslessAlpha = (qualityAlpha == AVIF_QUALITY_LOSSLESS);
     avifBool depthMatches = (sourceDepth == image->depth);
     avifBool using400 = (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400);
     avifBool using444 = (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV444);
@@ -1053,9 +1108,9 @@
     avifBool usingIdentityMatrix = (image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY);
 
     // Guess if the enduser is asking for lossless and enable it so that warnings can be emitted
-    if (!lossless && losslessColorQP && (!hasAlpha || losslessAlphaQP)) {
+    if (!lossless && usingLosslessColor && (!hasAlpha || usingLosslessAlpha)) {
         // The enduser is probably expecting lossless. Turn it on and emit warnings
-        printf("Min/max QPs set to %d, assuming --lossless to enable warnings on potential lossless issues.\n", AVIF_QUANTIZER_LOSSLESS);
+        printf("Quality set to %d, assuming --lossless to enable warnings on potential lossless issues.\n", AVIF_QUALITY_LOSSLESS);
         lossless = AVIF_TRUE;
     }
 
@@ -1066,17 +1121,17 @@
             lossless = AVIF_FALSE;
         }
 
-        if (!losslessColorQP) {
+        if (!usingLosslessColor) {
             fprintf(stderr,
-                    "WARNING: [--lossless] Color quantizer range (--min, --max) not set to %d. Color output might not be lossless.\n",
-                    AVIF_QUANTIZER_LOSSLESS);
+                    "WARNING: [--lossless] Color quality (-q or --qcolor) not set to %d. Color output might not be lossless.\n",
+                    AVIF_QUALITY_LOSSLESS);
             lossless = AVIF_FALSE;
         }
 
-        if (hasAlpha && !losslessAlphaQP) {
+        if (hasAlpha && !usingLosslessAlpha) {
             fprintf(stderr,
-                    "WARNING: [--lossless] Alpha present and alpha quantizer range (--minalpha, --maxalpha) not set to %d. Alpha output might not be lossless.\n",
-                    AVIF_QUANTIZER_LOSSLESS);
+                    "WARNING: [--lossless] Alpha present and alpha quality (--qalpha) not set to %d. Alpha output might not be lossless.\n",
+                    AVIF_QUALITY_LOSSLESS);
             lossless = AVIF_FALSE;
         }
 
@@ -1194,20 +1249,18 @@
     char manualTilingStr[128];
     snprintf(manualTilingStr, sizeof(manualTilingStr), "tileRowsLog2 [%d], tileColsLog2 [%d]", tileRowsLog2, tileColsLog2);
 
-    printf("Encoding with AV1 codec '%s' speed [%d], color QP [%d (%s) <-> %d (%s)], alpha QP [%d (%s) <-> %d (%s)], %s, %d worker thread(s), please wait...\n",
+    printf("Encoding with AV1 codec '%s' speed [%d], color quality [%d (%s)], alpha quality [%d (%s)], %s, %d worker thread(s), please wait...\n",
            avifCodecName(codecChoice, AVIF_CODEC_FLAG_CAN_ENCODE),
            speed,
-           minQuantizer,
-           quantizerString(minQuantizer),
-           maxQuantizer,
-           quantizerString(maxQuantizer),
-           minQuantizerAlpha,
-           quantizerString(minQuantizerAlpha),
-           maxQuantizerAlpha,
-           quantizerString(maxQuantizerAlpha),
+           quality,
+           qualityString(quality),
+           qualityAlpha,
+           qualityString(qualityAlpha),
            autoTiling ? "automatic tiling" : manualTilingStr,
            jobs);
     encoder->maxThreads = jobs;
+    encoder->quality = quality;
+    encoder->qualityAlpha = qualityAlpha;
     encoder->minQuantizer = minQuantizer;
     encoder->maxQuantizer = maxQuantizer;
     encoder->minQuantizerAlpha = minQuantizerAlpha;
diff --git a/examples/avif_example_encode.c b/examples/avif_example_encode.c
index 34b6cdc..0480ba4 100644
--- a/examples/avif_example_encode.c
+++ b/examples/avif_example_encode.c
@@ -77,15 +77,15 @@
     encoder = avifEncoderCreate();
     // Configure your encoder here (see avif/avif.h):
     // * maxThreads
-    // * minQuantizer
-    // * maxQuantizer
-    // * minQuantizerAlpha
-    // * maxQuantizerAlpha
+    // * quality
+    // * qualityAlpha
     // * tileRowsLog2
     // * tileColsLog2
     // * speed
     // * keyframeInterval
     // * timescale
+    encoder->quality = 60;
+    encoder->qualityAlpha = AVIF_QUALITY_LOSSLESS;
 
     // Call avifEncoderAddImage() for each image in your sequence
     // Only set AVIF_ADD_IMAGE_FLAG_SINGLE if you're not encoding a sequence
diff --git a/include/avif/avif.h b/include/avif/avif.h
index b91a9fa..d778fa4 100644
--- a/include/avif/avif.h
+++ b/include/avif/avif.h
@@ -78,6 +78,11 @@
 // a 12 hour AVIF image sequence, running at 60 fps (a basic sanity check as this is quite ridiculous)
 #define AVIF_DEFAULT_IMAGE_COUNT_LIMIT (12 * 3600 * 60)
 
+#define AVIF_QUALITY_DEFAULT -1
+#define AVIF_QUALITY_LOSSLESS 100
+#define AVIF_QUALITY_WORST 0
+#define AVIF_QUALITY_BEST 100
+
 #define AVIF_QUANTIZER_LOSSLESS 0
 #define AVIF_QUANTIZER_BEST_QUALITY 0
 #define AVIF_QUANTIZER_WORST_QUALITY 63
@@ -1066,7 +1071,14 @@
 // * If avifEncoderWrite() returns AVIF_RESULT_OK, output must be freed with avifRWDataFree()
 // * If (maxThreads < 2), multithreading is disabled
 //   * NOTE: Please see the "Understanding maxThreads" comment block above
-// * Quality range: [AVIF_QUANTIZER_BEST_QUALITY - AVIF_QUANTIZER_WORST_QUALITY]
+// * Quality range: [AVIF_QUALITY_WORST - AVIF_QUALITY_BEST]
+// * Quantizer range: [AVIF_QUANTIZER_BEST_QUALITY - AVIF_QUANTIZER_WORST_QUALITY]
+// * In older versions of libavif, the avifEncoder struct doesn't have the quality and qualityAlpha
+//   fields. For backward compatibility, if the quality field is not set, the default value of
+//   quality is based on the average of minQuantizer and maxQuantizer. Similarly the default value
+//   of qualityAlpha is based on the average of minQuantizerAlpha and maxQuantizerAlpha. New code
+//   should set quality and qualityAlpha and leave minQuantizer, maxQuantizer, minQuantizerAlpha,
+//   and maxQuantizerAlpha initialized to their default values.
 // * To enable tiling, set tileRowsLog2 > 0 and/or tileColsLog2 > 0.
 //   Tiling values range [0-6], where the value indicates a request for 2^n tiles in that dimension.
 //   If autoTiling is set to AVIF_TRUE, libavif ignores tileRowsLog2 and tileColsLog2 and
@@ -1093,6 +1105,8 @@
                           // played back `n + 1` times. Defaults to AVIF_REPETITION_COUNT_INFINITE.
 
     // changeable encoder settings
+    int quality;
+    int qualityAlpha;
     int minQuantizer;
     int maxQuantizer;
     int minQuantizerAlpha;
diff --git a/include/avif/internal.h b/include/avif/internal.h
index 6b89827..84974b0 100644
--- a/include/avif/internal.h
+++ b/include/avif/internal.h
@@ -289,6 +289,8 @@
     AVIF_ENCODER_CHANGE_MAX_QUANTIZER_ALPHA = (1u << 3),
     AVIF_ENCODER_CHANGE_TILE_ROWS_LOG2 = (1u << 4),
     AVIF_ENCODER_CHANGE_TILE_COLS_LOG2 = (1u << 5),
+    AVIF_ENCODER_CHANGE_QUANTIZER = (1u << 6),
+    AVIF_ENCODER_CHANGE_QUANTIZER_ALPHA = (1u << 7),
 
     AVIF_ENCODER_CHANGE_CODEC_SPECIFIC = (1u << 31)
 } avifEncoderChange;
@@ -308,6 +310,8 @@
 // encoder->tileRowsLog2, encoder->tileColsLog2, and encoder->autoTiling. The caller of
 // avifCodecEncodeImageFunc is responsible for automatic tiling if encoder->autoTiling is set to
 // AVIF_TRUE. The actual tiling values are passed to avifCodecEncodeImageFunc as parameters.
+// Similarly, avifCodecEncodeImageFunc should use the quantizer parameter instead of
+// encoder->quality and encoder->qualityAlpha.
 //
 // Note: The caller of avifCodecEncodeImageFunc always passes encoder->data->tileRowsLog2 and
 // encoder->data->tileColsLog2 as the tileRowsLog2 and tileColsLog2 arguments. Because
@@ -320,6 +324,7 @@
                                                avifBool alpha,
                                                int tileRowsLog2,
                                                int tileColsLog2,
+                                               int quantizer,
                                                avifEncoderChanges encoderChanges,
                                                avifAddImageFlags addImageFlags,
                                                avifCodecEncodeOutput * output);
diff --git a/src/codec_aom.c b/src/codec_aom.c
index c833082..586f1e5 100644
--- a/src/codec_aom.c
+++ b/src/codec_aom.c
@@ -64,12 +64,6 @@
     avifPixelFormatInfo formatInfo;
     aom_img_fmt_t aomFormat;
     avifBool monochromeEnabled;
-    // Whether cfg.rc_end_usage was set with an
-    // avifEncoderSetCodecSpecificOption(encoder, "end-usage", value) call.
-    avifBool endUsageSet;
-    // Whether cq-level was set with an
-    // avifEncoderSetCodecSpecificOption(encoder, "cq-level", value) call.
-    avifBool cqLevelSet;
     // Whether 'tuning' (of the specified distortion metric) was set with an
     // avifEncoderSetCodecSpecificOption(encoder, "tune", value) call.
     avifBool tuningSet;
@@ -376,7 +370,6 @@
                 return AVIF_FALSE;
             }
             cfg->rc_end_usage = val;
-            codec->internal->endUsageSet = AVIF_TRUE;
         }
     }
     return AVIF_TRUE;
@@ -466,9 +459,7 @@
                                   aom_codec_error_detail(&codec->internal->encoder));
             return AVIF_FALSE;
         }
-        if (!strcmp(key, "cq-level")) {
-            codec->internal->cqLevelSet = AVIF_TRUE;
-        } else if (!strcmp(key, "tune")) {
+        if (!strcmp(key, "tune")) {
             codec->internal->tuningSet = AVIF_TRUE;
         }
 #else  // !defined(HAVE_AOM_CODEC_SET_OPTION)
@@ -502,9 +493,7 @@
                 if (!success) {
                     return AVIF_FALSE;
                 }
-                if (aomOptionDefs[j].controlId == AOME_SET_CQ_LEVEL) {
-                    codec->internal->cqLevelSet = AVIF_TRUE;
-                } else if (aomOptionDefs[j].controlId == AOME_SET_TUNING) {
+                if (aomOptionDefs[j].controlId == AOME_SET_TUNING) {
                     codec->internal->tuningSet = AVIF_TRUE;
                 }
                 break;
@@ -526,6 +515,7 @@
                                       avifBool alpha,
                                       int tileRowsLog2,
                                       int tileColsLog2,
+                                      int quantizer,
                                       avifEncoderChanges encoderChanges,
                                       avifAddImageFlags addImageFlags,
                                       avifCodecEncodeOutput * output)
@@ -687,15 +677,6 @@
             cfg->g_threads = encoder->maxThreads;
         }
 
-        if (alpha) {
-            cfg->rc_min_quantizer = AVIF_CLAMP(encoder->minQuantizerAlpha, 0, 63);
-            cfg->rc_max_quantizer = AVIF_CLAMP(encoder->maxQuantizerAlpha, 0, 63);
-        } else {
-            cfg->rc_min_quantizer = AVIF_CLAMP(encoder->minQuantizer, 0, 63);
-            cfg->rc_max_quantizer = AVIF_CLAMP(encoder->maxQuantizer, 0, 63);
-        }
-        quantizerUpdated = AVIF_TRUE;
-
         codec->internal->monochromeEnabled = AVIF_FALSE;
         if (aomVersion > aomVersion_2_0_0) {
             // There exists a bug in libaom's chroma_check() function where it will attempt to
@@ -716,6 +697,32 @@
             return AVIF_RESULT_INVALID_CODEC_SPECIFIC_OPTION;
         }
 
+        int minQuantizer;
+        int maxQuantizer;
+        if (alpha) {
+            minQuantizer = encoder->minQuantizerAlpha;
+            maxQuantizer = encoder->maxQuantizerAlpha;
+        } else {
+            minQuantizer = encoder->minQuantizer;
+            maxQuantizer = encoder->maxQuantizer;
+        }
+        minQuantizer = AVIF_CLAMP(minQuantizer, 0, 63);
+        maxQuantizer = AVIF_CLAMP(maxQuantizer, 0, 63);
+        if ((cfg->rc_end_usage == AOM_VBR) || (cfg->rc_end_usage == AOM_CBR)) {
+            // cq-level is ignored in these two end-usage modes, so adjust minQuantizer and
+            // maxQuantizer to the target quantizer.
+            if (quantizer == AVIF_QUANTIZER_LOSSLESS) {
+                minQuantizer = AVIF_QUANTIZER_LOSSLESS;
+                maxQuantizer = AVIF_QUANTIZER_LOSSLESS;
+            } else {
+                minQuantizer = AVIF_MAX(quantizer - 4, minQuantizer);
+                maxQuantizer = AVIF_MIN(quantizer + 4, maxQuantizer);
+            }
+        }
+        cfg->rc_min_quantizer = minQuantizer;
+        cfg->rc_max_quantizer = maxQuantizer;
+        quantizerUpdated = AVIF_TRUE;
+
         aom_codec_flags_t encoderFlags = 0;
         if (image->depth > 8) {
             encoderFlags |= AOM_CODEC_USE_HIGHBITDEPTH;
@@ -729,7 +736,10 @@
         }
         codec->internal->encoderInitialized = AVIF_TRUE;
 
-        avifBool lossless = ((cfg->rc_min_quantizer == AVIF_QUANTIZER_LOSSLESS) && (cfg->rc_max_quantizer == AVIF_QUANTIZER_LOSSLESS));
+        if ((cfg->rc_end_usage == AOM_CQ) || (cfg->rc_end_usage == AOM_Q)) {
+            aom_codec_control(&codec->internal->encoder, AOME_SET_CQ_LEVEL, quantizer);
+        }
+        avifBool lossless = (quantizer == AVIF_QUANTIZER_LOSSLESS);
         if (lossless) {
             aom_codec_control(&codec->internal->encoder, AV1E_SET_LOSSLESS, 1);
         }
@@ -783,10 +793,20 @@
                 quantizerUpdated = AVIF_TRUE;
             }
         }
-        if (quantizerUpdated) {
-            avifBool lossless =
-                ((cfg->rc_min_quantizer == AVIF_QUANTIZER_LOSSLESS) && (cfg->rc_max_quantizer == AVIF_QUANTIZER_LOSSLESS));
-            aom_codec_control(&codec->internal->encoder, AV1E_SET_LOSSLESS, lossless);
+        const int quantizerChangedBit = alpha ? AVIF_ENCODER_CHANGE_QUANTIZER_ALPHA : AVIF_ENCODER_CHANGE_QUANTIZER;
+        if (encoderChanges & quantizerChangedBit) {
+            if ((cfg->rc_end_usage == AOM_VBR) || (cfg->rc_end_usage == AOM_CBR)) {
+                // cq-level is ignored in these two end-usage modes, so adjust minQuantizer and
+                // maxQuantizer to the target quantizer.
+                if (quantizer == AVIF_QUANTIZER_LOSSLESS) {
+                    cfg->rc_min_quantizer = AVIF_QUANTIZER_LOSSLESS;
+                    cfg->rc_max_quantizer = AVIF_QUANTIZER_LOSSLESS;
+                } else {
+                    cfg->rc_min_quantizer = AVIF_MAX(quantizer - 4, (int)cfg->rc_min_quantizer);
+                    cfg->rc_max_quantizer = AVIF_MIN(quantizer + 4, (int)cfg->rc_max_quantizer);
+                }
+                quantizerUpdated = AVIF_TRUE;
+            }
         }
         if (quantizerUpdated || dimensionsChanged) {
             aom_codec_err_t err = aom_codec_enc_config_set(&codec->internal->encoder, cfg);
@@ -804,6 +824,13 @@
         if (encoderChanges & AVIF_ENCODER_CHANGE_TILE_COLS_LOG2) {
             aom_codec_control(&codec->internal->encoder, AV1E_SET_TILE_COLUMNS, tileColsLog2);
         }
+        if (encoderChanges & quantizerChangedBit) {
+            if ((cfg->rc_end_usage == AOM_CQ) || (cfg->rc_end_usage == AOM_Q)) {
+                aom_codec_control(&codec->internal->encoder, AOME_SET_CQ_LEVEL, quantizer);
+            }
+            avifBool lossless = (quantizer == AVIF_QUANTIZER_LOSSLESS);
+            aom_codec_control(&codec->internal->encoder, AV1E_SET_LOSSLESS, lossless);
+        }
         if (encoderChanges & AVIF_ENCODER_CHANGE_CODEC_SPECIFIC) {
             if (!avifProcessAOMOptionsPostInit(codec, alpha)) {
                 return AVIF_RESULT_INVALID_CODEC_SPECIFIC_OPTION;
@@ -811,19 +838,6 @@
         }
     }
 
-#if defined(AOM_USAGE_ALL_INTRA)
-    if (quantizerUpdated && cfg->g_usage == AOM_USAGE_ALL_INTRA && !codec->internal->endUsageSet && !codec->internal->cqLevelSet) {
-        // The default rc_end_usage in all intra mode is AOM_Q, which requires cq-level to
-        // function. A libavif user may not know this internal detail and therefore may only
-        // set the min and max quantizers in the avifEncoder struct. If this is the case, set
-        // cq-level to a reasonable value for the user, otherwise the default cq-level
-        // (currently 10) will be unknowingly used.
-        assert(cfg->rc_end_usage == AOM_Q);
-        unsigned int cqLevel = (cfg->rc_min_quantizer + cfg->rc_max_quantizer) / 2;
-        aom_codec_control(&codec->internal->encoder, AOME_SET_CQ_LEVEL, cqLevel);
-    }
-#endif
-
     aom_image_t aomImage;
     // We prefer to simply set the aomImage.planes[] pointers to the plane buffers in 'image'. When
     // doing this, we set aomImage.w equal to aomImage.d_w and aomImage.h equal to aomImage.d_h and
diff --git a/src/codec_rav1e.c b/src/codec_rav1e.c
index 7e3a315..4b93b79 100644
--- a/src/codec_rav1e.c
+++ b/src/codec_rav1e.c
@@ -55,6 +55,7 @@
                                         avifBool alpha,
                                         int tileRowsLog2,
                                         int tileColsLog2,
+                                        int quantizer,
                                         avifEncoderChanges encoderChanges,
                                         uint32_t addImageFlags,
                                         avifCodecEncodeOutput * output)
@@ -139,17 +140,15 @@
         }
 
         int minQuantizer = AVIF_CLAMP(encoder->minQuantizer, 0, 63);
-        int maxQuantizer = AVIF_CLAMP(encoder->maxQuantizer, 0, 63);
         if (alpha) {
             minQuantizer = AVIF_CLAMP(encoder->minQuantizerAlpha, 0, 63);
-            maxQuantizer = AVIF_CLAMP(encoder->maxQuantizerAlpha, 0, 63);
         }
         minQuantizer = (minQuantizer * 255) / 63; // Rescale quantizer values as rav1e's QP range is [0,255]
-        maxQuantizer = (maxQuantizer * 255) / 63;
+        quantizer = (quantizer * 255) / 63;
         if (rav1e_config_parse_int(rav1eConfig, "min_quantizer", minQuantizer) == -1) {
             goto cleanup;
         }
-        if (rav1e_config_parse_int(rav1eConfig, "quantizer", maxQuantizer) == -1) {
+        if (rav1e_config_parse_int(rav1eConfig, "quantizer", quantizer) == -1) {
             goto cleanup;
         }
         if (tileRowsLog2 != 0) {
diff --git a/src/codec_svt.c b/src/codec_svt.c
index 6a3ba85..158b2a4 100644
--- a/src/codec_svt.c
+++ b/src/codec_svt.c
@@ -48,6 +48,7 @@
                                       avifBool alpha,
                                       int tileRowsLog2,
                                       int tileColsLog2,
+                                      int quantizer,
                                       avifEncoderChanges encoderChanges,
                                       uint32_t addImageFlags,
                                       avifCodecEncodeOutput * output)
@@ -136,7 +137,7 @@
             svt_config->min_qp_allowed = AVIF_CLAMP(encoder->minQuantizer, 0, 63);
             svt_config->max_qp_allowed = AVIF_CLAMP(encoder->maxQuantizer, 0, 63);
         }
-        svt_config->qp = (svt_config->min_qp_allowed + svt_config->max_qp_allowed) / 2;
+        svt_config->qp = quantizer;
 
         if (tileRowsLog2 != 0) {
             svt_config->tile_rows = tileRowsLog2;
diff --git a/src/write.c b/src/write.c
index 66ad49e..5484e46 100644
--- a/src/write.c
+++ b/src/write.c
@@ -204,10 +204,17 @@
 {
     avifEncoderItemArray items;
     avifEncoderFrameArray frames;
+    // Map the encoder settings quality and qualityAlpha to quantizer and quantizerAlpha
+    int quantizer;
+    int quantizerAlpha;
     // tileRowsLog2 and tileColsLog2 are the actual tiling values after automatic tiling is handled
     int tileRowsLog2;
     int tileColsLog2;
     avifEncoder lastEncoder;
+    // lastQuantizer and lastQuantizerAlpha are the quantizer and quantizerAlpha values used last
+    // time
+    int lastQuantizer;
+    int lastQuantizerAlpha;
     // lastTileRowsLog2 and lastTileColsLog2 are the actual tiling values used last time
     int lastTileRowsLog2;
     int lastTileColsLog2;
@@ -385,10 +392,12 @@
     encoder->speed = AVIF_SPEED_DEFAULT;
     encoder->keyframeInterval = 0;
     encoder->timescale = 1;
-    encoder->minQuantizer = AVIF_QUANTIZER_LOSSLESS;
-    encoder->maxQuantizer = AVIF_QUANTIZER_LOSSLESS;
-    encoder->minQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS;
-    encoder->maxQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS;
+    encoder->quality = AVIF_QUALITY_DEFAULT;
+    encoder->qualityAlpha = AVIF_QUALITY_DEFAULT;
+    encoder->minQuantizer = AVIF_QUANTIZER_BEST_QUALITY;
+    encoder->maxQuantizer = AVIF_QUANTIZER_WORST_QUALITY;
+    encoder->minQuantizerAlpha = AVIF_QUANTIZER_BEST_QUALITY;
+    encoder->maxQuantizerAlpha = AVIF_QUANTIZER_WORST_QUALITY;
     encoder->tileRowsLog2 = 0;
     encoder->tileColsLog2 = 0;
     encoder->autoTiling = AVIF_FALSE;
@@ -425,6 +434,8 @@
     lastEncoder->maxQuantizer = encoder->maxQuantizer;
     lastEncoder->minQuantizerAlpha = encoder->minQuantizerAlpha;
     lastEncoder->maxQuantizerAlpha = encoder->maxQuantizerAlpha;
+    encoder->data->lastQuantizer = encoder->data->quantizer;
+    encoder->data->lastQuantizerAlpha = encoder->data->quantizerAlpha;
     encoder->data->lastTileRowsLog2 = encoder->data->tileRowsLog2;
     encoder->data->lastTileColsLog2 = encoder->data->tileColsLog2;
 }
@@ -448,6 +459,12 @@
         return AVIF_FALSE;
     }
 
+    if (encoder->data->lastQuantizer != encoder->data->quantizer) {
+        *encoderChanges |= AVIF_ENCODER_CHANGE_QUANTIZER;
+    }
+    if (encoder->data->lastQuantizerAlpha != encoder->data->quantizerAlpha) {
+        *encoderChanges |= AVIF_ENCODER_CHANGE_QUANTIZER_ALPHA;
+    }
     if (lastEncoder->minQuantizer != encoder->minQuantizer) {
         *encoderChanges |= AVIF_ENCODER_CHANGE_MIN_QUANTIZER;
     }
@@ -811,6 +828,21 @@
     return dstImage;
 }
 
+static int avifQualityToQuantizer(int quality, int minQuantizer, int maxQuantizer)
+{
+    int quantizer;
+    if (quality == AVIF_QUALITY_DEFAULT) {
+        // In older libavif releases, avifEncoder didn't have the quality and qualityAlpha fields.
+        // Supply a default value for quantizer.
+        quantizer = (minQuantizer + maxQuantizer) / 2;
+        quantizer = AVIF_CLAMP(quantizer, 0, 63);
+    } else {
+        quality = AVIF_CLAMP(quality, 0, 100);
+        quantizer = ((100 - quality) * 63 + 50) / 100;
+    }
+    return quantizer;
+}
+
 static avifResult avifEncoderAddImageInternal(avifEncoder * encoder,
                                               uint32_t gridCols,
                                               uint32_t gridRows,
@@ -914,6 +946,11 @@
     }
 
     // -----------------------------------------------------------------------
+    // Map quality and qualityAlpha to quantizer and quantizerAlpha
+    encoder->data->quantizer = avifQualityToQuantizer(encoder->quality, encoder->minQuantizer, encoder->maxQuantizer);
+    encoder->data->quantizerAlpha = avifQualityToQuantizer(encoder->qualityAlpha, encoder->minQuantizerAlpha, encoder->maxQuantizerAlpha);
+
+    // -----------------------------------------------------------------------
     // Handle automatic tiling
 
     encoder->data->tileRowsLog2 = AVIF_CLAMP(encoder->tileRowsLog2, 0, 6);
@@ -1120,12 +1157,14 @@
                 }
                 cellImage = paddedCellImage;
             }
+            const int quantizer = item->alpha ? encoder->data->quantizerAlpha : encoder->data->quantizer;
             avifResult encodeResult = item->codec->encodeImage(item->codec,
                                                                encoder,
                                                                cellImage,
                                                                item->alpha,
                                                                encoder->data->tileRowsLog2,
                                                                encoder->data->tileColsLog2,
+                                                               quantizer,
                                                                encoderChanges,
                                                                addImageFlags,
                                                                item->encodeOutput);
diff --git a/tests/gtest/avifgridapitest.cc b/tests/gtest/avifgridapitest.cc
index c69994b..6c606d9 100644
--- a/tests/gtest/avifgridapitest.cc
+++ b/tests/gtest/avifgridapitest.cc
@@ -42,10 +42,8 @@
     return AVIF_RESULT_OUT_OF_MEMORY;
   }
   encoder->speed = AVIF_SPEED_FASTEST;
-  encoder->minQuantizer = AVIF_QUANTIZER_LOSSLESS;
-  encoder->maxQuantizer = AVIF_QUANTIZER_LOSSLESS;
-  encoder->minQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS;
-  encoder->minQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS;
+  encoder->quality = AVIF_QUALITY_LOSSLESS;
+  encoder->qualityAlpha = AVIF_QUALITY_LOSSLESS;
   // cell_image_ptrs exists only to match the libavif API.
   std::vector<avifImage*> cell_image_ptrs(cell_images.size());
   for (size_t i = 0; i < cell_images.size(); ++i) {
diff --git a/tests/test_cmd.sh b/tests/test_cmd.sh
index 67b5e16..1e5d70b 100755
--- a/tests/test_cmd.sh
+++ b/tests/test_cmd.sh
@@ -51,11 +51,12 @@
 ENCODED_FILE="avif_test_cmd_encoded.avif"
 ENCODED_FILE_WITH_DASH="-avif_test_cmd_encoded.avif"
 DECODED_FILE="avif_test_cmd_decoded.png"
+OUT_MSG="avif_test_cmd_out_msg.txt"
 
 # Cleanup
 cleanup() {
   pushd ${TMP_DIR}
-    rm -- "${ENCODED_FILE}" "${ENCODED_FILE_WITH_DASH}" "${DECODED_FILE}"
+    rm -- "${ENCODED_FILE}" "${ENCODED_FILE_WITH_DASH}" "${DECODED_FILE}" "${OUT_MSG}"
   popd
 }
 trap cleanup EXIT
@@ -81,6 +82,21 @@
   # --minalpha and --maxalpha must be both specified.
   "${AVIFENC}" -s 10 --minalpha 0 "${INPUT_PNG}" "${ENCODED_FILE}" && exit 1
   "${AVIFENC}" -s 10 --maxalpha 0 "${INPUT_PNG}" "${ENCODED_FILE}" && exit 1
+
+  # The default quality is 60. The default alpha quality is 100 (lossless).
+  "${AVIFENC}" -s 10 "${INPUT_Y4M}" "${ENCODED_FILE}" > "${OUT_MSG}"
+  grep " color quality \[60 " "${OUT_MSG}"
+  grep " alpha quality \[100 " "${OUT_MSG}"
+  "${AVIFENC}" -s 10 -q 85 "${INPUT_Y4M}" "${ENCODED_FILE}" > "${OUT_MSG}"
+  grep " color quality \[85 " "${OUT_MSG}"
+  grep " alpha quality \[100 " "${OUT_MSG}"
+  # The average of 15 and 25 is 20. Quantizer 20 maps to quality 68.
+  "${AVIFENC}" -s 10 --min 15 --max 25 "${INPUT_Y4M}" "${ENCODED_FILE}" > "${OUT_MSG}"
+  grep " color quality \[68 " "${OUT_MSG}"
+  grep " alpha quality \[100 " "${OUT_MSG}"
+  "${AVIFENC}" -s 10 -q 65 --min 15 --max 25 "${INPUT_Y4M}" "${ENCODED_FILE}" > "${OUT_MSG}"
+  grep " color quality \[65 " "${OUT_MSG}"
+  grep " alpha quality \[100 " "${OUT_MSG}"
 popd
 
 exit 0