diff --git a/api/funkwhale_api/music/dynamic_preferences_registry.py b/api/funkwhale_api/music/dynamic_preferences_registry.py
new file mode 100644
index 0000000000000000000000000000000000000000..d41225775ba4f4b93a22bf91075063be4ba51366
--- /dev/null
+++ b/api/funkwhale_api/music/dynamic_preferences_registry.py
@@ -0,0 +1,19 @@
+from dynamic_preferences import types
+from dynamic_preferences.registries import global_preferences_registry
+
+music = types.Section("music")
+
+
+@global_preferences_registry.register
+class MaxTracks(types.BooleanPreference):
+    show_in_api = True
+    section = music
+    name = "transcoding_enabled"
+    verbose_name = "Transcoding enabled"
+    help_text = (
+        "Enable transcoding of audio files in formats requested by the client. "
+        "This is especially useful for devices that do not support formats "
+        "such as Flac or Ogg, but the transcoding process will increase the "
+        "load on the server."
+    )
+    default = True
diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py
index 71c511d33a123af90374ac39e036d3ba4a963550..c99bfe2c1913da7ec633a044fcc1f9234347bbb7 100644
--- a/api/funkwhale_api/music/models.py
+++ b/api/funkwhale_api/music/models.py
@@ -11,7 +11,7 @@ from django.conf import settings
 from django.contrib.postgres.fields import JSONField
 from django.core.files.base import ContentFile
 from django.core.serializers.json import DjangoJSONEncoder
-from django.db import models
+from django.db import models, transaction
 from django.db.models.signals import post_save
 from django.dispatch import receiver
 from django.urls import reverse
@@ -744,6 +744,37 @@ class Upload(models.Model):
     def listen_url(self):
         return self.track.listen_url + "?upload={}".format(self.uuid)
 
+    def get_transcoded_version(self, format):
+        mimetype = utils.EXTENSION_TO_MIMETYPE[format]
+        existing_versions = list(self.versions.filter(mimetype=mimetype))
+        if existing_versions:
+            # we found an existing version, no need to transcode again
+            return existing_versions[0]
+
+        return self.create_transcoded_version(mimetype, format)
+
+    @transaction.atomic
+    def create_transcoded_version(self, mimetype, format):
+        # we create the version with an empty file, then
+        # we'll write to it
+        f = ContentFile(b"")
+        version = self.versions.create(mimetype=mimetype, bitrate=self.bitrate or 128000, size=0)
+        # we keep the same name, but we update the extension
+        new_name = os.path.splitext(
+            os.path.basename(self.audio_file.name)
+        )[0] + '.{}'.format(format)
+        version.audio_file.save(new_name, f)
+        utils.transcode_file(
+            input=self.audio_file,
+            output=version.audio_file,
+            input_format=utils.MIMETYPE_TO_EXTENSION[self.mimetype],
+            output_format=utils.MIMETYPE_TO_EXTENSION[mimetype],
+        )
+        version.size = version.audio_file.size
+        version.save(update_fields=['size'])
+
+        return version
+
 
 MIMETYPE_CHOICES = [
     (mt, ext) for ext, mt in utils.AUDIO_EXTENSIONS_AND_MIMETYPE
diff --git a/api/funkwhale_api/music/utils.py b/api/funkwhale_api/music/utils.py
index 6da9ad9493647ad398a35e27faa8f7d04fb0eb7f..f146dc344a6898e279d0fd195d88cc0afba89f6e 100644
--- a/api/funkwhale_api/music/utils.py
+++ b/api/funkwhale_api/music/utils.py
@@ -2,8 +2,10 @@ import mimetypes
 
 import magic
 import mutagen
+import pydub
 
 from funkwhale_api.common.search import normalize_query, get_query  # noqa
+from funkwhale_api.common import utils
 
 
 def guess_mimetype(f):
@@ -68,3 +70,10 @@ def get_actor_from_request(request):
         actor = request.user.actor
 
     return actor
+
+
+def transcode_file(input, output, input_format, output_format, **kwargs):
+    with input.open("rb"):
+        audio = pydub.AudioSegment.from_file(input, format=input_format)
+    with output.open("wb"):
+        return audio.export(output, format=output_format, **kwargs)
diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index c8d1b94fc39a9b0c98f559b2d0b098c35515cd09..c5cfeb25cf16a7c0b1566ad2ca65d98bd781a503 100644
--- a/api/funkwhale_api/music/views.py
+++ b/api/funkwhale_api/music/views.py
@@ -15,8 +15,9 @@ from rest_framework.decorators import detail_route, list_route
 from rest_framework.response import Response
 from taggit.models import Tag
 
-from funkwhale_api.common import utils as common_utils
 from funkwhale_api.common import permissions as common_permissions
+from funkwhale_api.common import preferences
+from funkwhale_api.common import utils as common_utils
 from funkwhale_api.federation.authentication import SignatureAuthentication
 from funkwhale_api.federation import api_serializers as federation_api_serializers
 from funkwhale_api.federation import routes
@@ -267,12 +268,31 @@ def get_file_path(audio_file):
         return path.encode("utf-8")
 
 
-def handle_serve(upload, user):
+def should_transcode(upload, format):
+    if not preferences.get("music__transcoding_enabled"):
+        return False
+    if format is None:
+        return False
+    if format not in utils.EXTENSION_TO_MIMETYPE:
+        # format should match supported formats
+        return False
+    if upload.mimetype is None:
+        # upload should have a mimetype, otherwise we cannot transcode
+        return False
+    if upload.mimetype == utils.EXTENSION_TO_MIMETYPE[format]:
+        # requested format sould be different than upload mimetype, otherwise
+        # there is no need to transcode
+        return False
+    return True
+
+
+def handle_serve(upload, user, format=None):
     f = upload
     # we update the accessed_date
-    f.accessed_date = timezone.now()
-    f.save(update_fields=["accessed_date"])
-
+    now = timezone.now()
+    upload.accessed_date = now
+    upload.save(update_fields=["accessed_date"])
+    f = upload
     if f.audio_file:
         file_path = get_file_path(f.audio_file)
 
@@ -298,6 +318,14 @@ def handle_serve(upload, user):
     elif f.source and f.source.startswith("file://"):
         file_path = get_file_path(f.source.replace("file://", "", 1))
     mt = f.mimetype
+
+    if should_transcode(f, format):
+        transcoded_version = upload.get_transcoded_version(format)
+        transcoded_version.accessed_date = now
+        transcoded_version.save(update_fields=["accessed_date"])
+        f = transcoded_version
+        file_path = get_file_path(f.audio_file)
+        mt = f.mimetype
     if mt:
         response = Response(content_type=mt)
     else:
@@ -337,7 +365,8 @@ class ListenViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
         if not upload:
             return Response(status=404)
 
-        return handle_serve(upload, user=request.user)
+        format = request.GET.get("to")
+        return handle_serve(upload, user=request.user, format=format)
 
 
 class UploadViewSet(
diff --git a/api/requirements/base.txt b/api/requirements/base.txt
index 246525b99070a02d5b6a456a318f694231b1b24a..06fbd4cc452659486cfdad4ddf748b55e5fdb6f2 100644
--- a/api/requirements/base.txt
+++ b/api/requirements/base.txt
@@ -69,3 +69,4 @@ django-cleanup==2.1.0
 # for LDAP authentication
 python-ldap==3.1.0
 django-auth-ldap==1.7.0
+pydub==0.23.0
diff --git a/api/tests/music/test_views.py b/api/tests/music/test_views.py
index 389306820268157cc2e3d40a08bcc980edd4625d..d4ce1e2bd3c5be562d446ef2874b9905616d5aeb 100644
--- a/api/tests/music/test_views.py
+++ b/api/tests/music/test_views.py
@@ -1,11 +1,12 @@
 import io
+import magic
 import os
 
 import pytest
 from django.urls import reverse
 from django.utils import timezone
 
-from funkwhale_api.music import serializers, tasks, views
+from funkwhale_api.music import models, serializers, tasks, views
 from funkwhale_api.federation import api_serializers as federation_api_serializers
 
 DATA_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -309,7 +310,69 @@ def test_listen_explicit_file(factories, logged_in_api_client, mocker):
     response = logged_in_api_client.get(url, {"upload": upload2.uuid})
 
     assert response.status_code == 200
-    mocked_serve.assert_called_once_with(upload2, user=logged_in_api_client.user)
+    mocked_serve.assert_called_once_with(
+        upload2, user=logged_in_api_client.user, format=None
+    )
+
+
+@pytest.mark.parametrize(
+    "mimetype,format,expected",
+    [
+        # already in proper format
+        ("audio/mpeg", "mp3", False),
+        # empty mimetype / format
+        (None, "mp3", False),
+        ("audio/mpeg", None, False),
+        # unsupported format
+        ("audio/mpeg", "noop", False),
+        # should transcode
+        ("audio/mpeg", "ogg", True),
+    ],
+)
+def test_should_transcode(mimetype, format, expected, factories):
+    upload = models.Upload(mimetype=mimetype)
+    assert views.should_transcode(upload, format) is expected
+
+
+@pytest.mark.parametrize("value", [True, False])
+def test_should_transcode_according_to_preference(value, preferences, factories):
+    upload = models.Upload(mimetype="audio/ogg")
+    expected = value
+    preferences["music__transcoding_enabled"] = value
+
+    assert views.should_transcode(upload, "mp3") is expected
+
+
+def test_handle_serve_create_mp3_version(factories, now):
+    user = factories["users.User"]()
+    upload = factories["music.Upload"](bitrate=42)
+    response = views.handle_serve(upload, user, format="mp3")
+
+    version = upload.versions.latest("id")
+
+    assert version.mimetype == "audio/mpeg"
+    assert version.accessed_date == now
+    assert version.bitrate == upload.bitrate
+    assert version.audio_file.path.endswith(".mp3")
+    assert version.size == version.audio_file.size
+    assert magic.from_buffer(version.audio_file.read(), mime=True) == "audio/mpeg"
+
+    assert response.status_code == 200
+
+
+def test_listen_transcode(factories, now, logged_in_api_client, mocker):
+    upload = factories["music.Upload"](
+        import_status="finished", library__actor__user=logged_in_api_client.user
+    )
+    url = reverse("api:v1:listen-detail", kwargs={"uuid": upload.track.uuid})
+    handle_serve = mocker.spy(views, "handle_serve")
+    response = logged_in_api_client.get(url, {"to": "mp3"})
+
+    assert response.status_code == 200
+
+    handle_serve.assert_called_once_with(
+        upload, user=logged_in_api_client.user, format="mp3"
+    )
 
 
 def test_user_can_create_library(factories, logged_in_api_client):