# Building LAME 4.0 the way SongShrink does

SongShrink (iPhone, iPad, Mac and Android) exports MP3 files with the LAME MP3
encoder, version 4.0. LAME is licensed under the GNU Library General Public
License version 2 (see `LAME-COPYING.txt`; the GNU Lesser General Public
License version 2.1 is in `LGPL-2.1.txt`). This page provides LAME's source
and the files to build it, and nothing else of SongShrink: the exact LAME
source the app uses, the files and settings the app builds it with, the LAME
functions the app calls, and how a rebuilt LAME takes the place of the app's
copy.

## What this page does not include

- **SongShrink's own code.** There is no SongShrink source code or object code
  here, only LAME and the files that build it.
- **A relinkable iPhone or iPad app.** An iPhone or iPad app downloaded from
  the App Store is encrypted by Apple, so that copy is not in a form you can
  relink: replacing `LameKit.framework` inside it and signing it again does not
  give you a working app.
- The Mac and Android steps below were checked on builds Duke & Duke made
  (section 2 and 3 say exactly what was checked), not on copies downloaded
  from the Mac App Store or Google Play.

## What the app uses

- **The official, unmodified LAME 4.0 release:** `lame-4.0.tar.gz`, exactly as
  published on SourceForge
  (https://sourceforge.net/projects/lame/files/lame/4.0/lame-4.0.tar.gz/download).
  - SHA-256 `3df5124d5ad3a98312ffd7ba6a9b36230e4f8a3e66d3ce0f425e336c32d216eb`
  - SHA-1 `e3630adf399d8917d4da19b96937b7f9b12774ea`
  - MD5 `4b82945ffc410cb5d3947b9d6f890ae8`
  - 1,496,810 bytes
- **The encoder library only** (`libmp3lame`). No decoder (mpglib / mpg123), no
  command-line program, no assembler.
- **One file that is not from LAME:** `config.h`. LAME's `./configure` normally
  writes this file; SongShrink uses a hand-written one instead (in
  `build-files/LameKit/config.h`). It is the same on every platform.
- **A separate library:** `LameKit.framework` on iPhone, iPad and Mac, and
  `liblame.so` on Android. The app calls LAME only through that library, so you
  can replace it.

Check your download:

```sh
shasum -a 256 -c SHA256SUMS
```

## Files

| File | What it is |
| --- | --- |
| `lame-4.0.tar.gz` | The official LAME 4.0 source release, unmodified |
| `build-files/LameKit/config.h` | The build configuration (the only file not from LAME) |
| `build-files/LameKit/Package.swift` | How the iPhone, iPad and Mac app builds LAME |
| `build-files/LameKit/LAME-VERSION.txt` | Which LAME files the app builds |
| `build-files/android/CMakeLists.txt` | How the Android app builds LAME (its LAME part, on its own) |
| `LAME-COPYING.txt` | LAME's licence (the `COPYING` file from the release) |
| `LGPL-2.1.txt` | The GNU Lesser General Public License version 2.1 |
| `SHA256SUMS` | SHA-256 of every file above |

## 1. Prepare the source

Put the files from this page in one folder, then, in that folder:

```sh
tar -xzf lame-4.0.tar.gz
mkdir -p LameKit/Sources/CLame/include LameKit/Sources/CLame/vector
cp lame-4.0/libmp3lame/*.c lame-4.0/libmp3lame/*.h LameKit/Sources/CLame/
rm LameKit/Sources/CLame/mpglib_interface.c
cp lame-4.0/libmp3lame/vector/lame_intrin.h LameKit/Sources/CLame/vector/
cp lame-4.0/include/lame.h LameKit/Sources/CLame/include/
cp build-files/LameKit/config.h LameKit/Sources/CLame/
cp build-files/LameKit/Package.swift LameKit/
mkdir -p android && cp build-files/android/CMakeLists.txt android/
```

`LameKit/Sources/CLame` now holds exactly the files the app is built from:
every `.c` and `.h` file directly in `lame-4.0/libmp3lame` except
`mpglib_interface.c` (the decoder interface), plus `vector/lame_intrin.h`,
`include/lame.h` and `config.h`. Not used from `libmp3lame`:
`mpglib_interface.c`; `vector/xmm_quantize_sub.c` (SSE code, only built when
`HAVE_XMMINTRIN_H` is defined, which `config.h` leaves undefined); the `i386`
folder (assembler files and `nasm.h`); `lame.rc`, `logoe.ico`, `lame.pc.in`,
`depcomp` and the Makefiles. To build a modified LAME, change the files in
`LameKit/Sources/CLame`.

## 2. iPhone, iPad and Mac

### How SongShrink builds it

`LameKit` is a Swift package (`build-files/LameKit/Package.swift`):

- one C target, `CLame`, with the sources above;
- C settings: `HAVE_CONFIG_H` defined, header search path `.`;
- product `LameKit`, type **dynamic**;
- platforms iOS 17.0 and macOS 14.0.

The app is built with Xcode 27.0. The app itself requires iOS 27 and macOS 27
(its deployment target), but the embedded `LameKit.framework` keeps the
package's own minimums: it is built for iOS 17.0 and macOS 14.0 (its
`MinimumOSVersion` / `LSMinimumSystemVersion` and its Mach-O minimum OS),
as the commands below also build it. Xcode builds the package as
`LameKit.framework` (bundle identifier `lamekit.LameKit`) and copies it into
the app, signed, at:

- iPhone and iPad: `SongShrink.app/Frameworks/LameKit.framework`
  (install name `@rpath/LameKit.framework/LameKit`)
- Mac: `SongShrink.app/Contents/Frameworks/LameKit.framework`
  (install name `@rpath/LameKit.framework/Versions/A/LameKit`)

The app finds it through its runpath: `@executable_path/Frameworks` on iPhone
and iPad, `@executable_path/../Frameworks` on the Mac.

### Build it

```sh
cd LameKit
xcodebuild -scheme LameKit -destination 'generic/platform=iOS' \
  -configuration Release -derivedDataPath ../dd CODE_SIGNING_ALLOWED=NO build
xcodebuild -scheme LameKit -destination 'generic/platform=macOS' \
  -configuration Release -derivedDataPath ../dd build
cd ..
```

The frameworks are then at
`dd/Build/Products/Release-iphoneos/PackageFrameworks/LameKit.framework`
(iPhone and iPad) and
`dd/Build/Products/Release/PackageFrameworks/LameKit.framework` (Mac).

### Functions the app calls

A replacement must be a framework named `LameKit.framework` with the bundle
identifier `lamekit.LameKit` and the install name above (building it with
`build-files/LameKit/Package.swift`, as shown, gives all three), and it must
export these functions, with the signatures in `lame.h`:

`lame_init`, `lame_init_params`, `lame_close`, `lame_set_in_samplerate`,
`lame_set_out_samplerate`, `lame_set_num_channels`, `lame_set_mode`,
`lame_set_brate`, `lame_set_VBR`, `lame_set_quality`, `lame_set_bWriteVbrTag`,
`lame_encode_buffer_interleaved_ieee_float`, `lame_encode_flush`,
`lame_get_lametag_frame`.

### Run the app with your LAME (Mac)

The Mac app is signed with the hardened runtime, which only loads libraries
signed by the same team as the app. So a replaced framework only loads if the
framework and the app are signed again by the same signer. Two ways:

- **With your own Apple signing identity:** sign the new framework, the
  downloader extension (`SongShrink.app/Contents/Extensions/SongShrinkAssetDownloader.appex`)
  and then the app with that identity, keeping the hardened runtime
  (`codesign --force --options runtime --sign "<your identity>" …`, the app
  with its entitlements as below). Duke & Duke has not tested this way.
- **Without one,** sign the framework and the app ad hoc, as below. An ad-hoc
  signature has no team, and ad-hoc signing as shown leaves the hardened
  runtime off; with it on, macOS refuses to load an ad-hoc framework.

```sh
rm -r SongShrink.app/Contents/Frameworks/LameKit.framework
cp -R dd/Build/Products/Release/PackageFrameworks/LameKit.framework SongShrink.app/Contents/Frameworks/
codesign -d --entitlements - --xml SongShrink.app > entitlements.plist
codesign --force --sign - SongShrink.app/Contents/Frameworks/LameKit.framework
codesign --force --sign - --entitlements entitlements.plist SongShrink.app
codesign --verify --deep --strict SongShrink.app
```

Duke & Duke checked these commands on a release build of the Mac app: macOS
loaded the replacement `LameKit.framework` into the re-signed copy, and then
showed a system prompt before the app finished starting (a copy signed by
someone else uses the same bundle identifier as the original app), so a full
start of such a copy has not been confirmed. Keeping Duke & Duke's signature
on the app, or signing ad hoc with the hardened runtime on, makes macOS refuse
the framework ("different Team IDs"). Limits of
a copy signed by you: the vocal removal model is downloaded through an app
group belonging to Duke & Duke's developer team, which a copy signed by anyone
else is not entitled to, so expect the model download not to work in it (not
tested); and the App Store won't update it.

### Run the app with your LAME (iPhone and iPad)

The framework goes in the same place inside the app,
`SongShrink.app/Frameworks/LameKit.framework`. As said at the top, the copy
of the app you download from the App Store is encrypted by Apple and can't be
relinked this way, and this page does not include an unencrypted copy of the
app or its object code. An iPhone or iPad also only runs apps signed with a
certificate it trusts, so any app you change has to be signed with your own
Apple developer signing identity and provisioning profile.

## 3. Android

### How SongShrink builds it

The Android app builds LAME with the Android NDK and CMake, from the same
source folder, as its own shared library `liblame.so`. Its build file
`android/app/src/main/cpp/CMakeLists.txt` says, for LAME:

```cmake
cmake_minimum_required(VERSION 3.22.1)
project(songshrink_lame C)
set(LAME_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../LameKit/Sources/CLame)
file(GLOB LAME_SRC ${LAME_DIR}/*.c)
add_library(lame SHARED ${LAME_SRC})
target_include_directories(lame PUBLIC ${LAME_DIR} ${LAME_DIR}/include)
target_compile_definitions(lame PRIVATE HAVE_CONFIG_H)
target_compile_options(lame PRIVATE -O3 -w)
target_link_libraries(lame m)
target_link_options(lame PRIVATE "-Wl,-z,max-page-size=16384")
```

The same file also builds the app's own small bridge, `libsongshrink_mp3.so`,
which links to `liblame.so`. `build-files/android/CMakeLists.txt` on this page is
the LAME part above, with `LAME_DIR` pointing at the folder from step 1.

Other settings (from `android/app/build.gradle.kts`):

- Android NDK 27.2.12479018, CMake 3.22.1
- minimum Android version API 29 (`ANDROID_PLATFORM=android-29`)
- ABIs `arm64-v8a` and `x86_64`
- release builds use CMake build type `RelWithDebInfo` (the Android Gradle
  plugin's default), and Gradle strips the library with `llvm-strip` when it
  packages the app.

The CMake commands below are the authoritative way to reproduce the build:
the NDK's toolchain file supplies the compiler, target and sysroot. For
reference, the flags CMake listed for each LAME file in the app's release
build (besides the NDK's target and sysroot settings) were:
`-g -DANDROID -fdata-sections -ffunction-sections -funwind-tables
-fstack-protector-strong -no-canonical-prefixes -D_FORTIFY_SOURCE=2 -Wformat
-Werror=format-security -O2 -g -DNDEBUG -fPIC -O3 -w`, with
`-DHAVE_CONFIG_H -Dlame_EXPORTS`.

### Build it

With the Android SDK's NDK and CMake (`$ANDROID_HOME` is your Android SDK):

```sh
NDK=$ANDROID_HOME/ndk/27.2.12479018
CMAKE=$ANDROID_HOME/cmake/3.22.1/bin
for ABI in arm64-v8a x86_64; do
  $CMAKE/cmake -S android -B build-$ABI -G Ninja -DCMAKE_MAKE_PROGRAM=$CMAKE/ninja \
    -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \
    -DANDROID_ABI=$ABI -DANDROID_PLATFORM=android-29 -DCMAKE_BUILD_TYPE=RelWithDebInfo
  $CMAKE/cmake --build build-$ABI
done
```

Each `build-<ABI>/liblame.so` is the library. To strip it as Gradle does, run
`llvm-strip --strip-unneeded` on it (from
`$NDK/toolchains/llvm/prebuilt/<your computer>/bin`). Built from the
unmodified source this way and stripped, each library matches the one in the
app except for its 20-byte build ID.

### Functions the app calls

The app loads `liblame` first (`System.loadLibrary("lame")`), then its bridge
`libsongshrink_mp3.so`, which finds LAME by its name `liblame.so`. A
replacement must keep that name and export these functions, with the
signatures in `lame.h`:

`get_lame_version`, `lame_init`, `lame_init_params`, `lame_close`,
`lame_set_in_samplerate`, `lame_set_out_samplerate`, `lame_set_num_channels`,
`lame_set_mode`, `lame_set_brate`, `lame_set_VBR`, `lame_set_quality`,
`lame_set_bWriteVbrTag`, `lame_set_write_id3tag_automatic`,
`lame_encode_buffer_interleaved`, `lame_encode_flush`,
`lame_get_lametag_frame`.

### Run the app with your LAME

`liblame.so` is stored uncompressed in the app's APK at
`lib/<ABI>/liblame.so`. When the app is installed from Google Play, it arrives
as several APKs; `adb shell pm path uk.dukeandduke.songshrink` lists them, and
the library is in the one for your device's ABI (for example
`split_config.arm64_v8a.apk`). Copy them all with `adb pull`, then, for the
APK that holds the library (`app.apk` here):

```sh
mkdir -p lib/arm64-v8a && cp build-arm64-v8a/liblame.so lib/arm64-v8a/
zip -0 -X app.apk lib/arm64-v8a/liblame.so
zipalign -f -P 16 4 app.apk app-aligned.apk
```

Sign every APK of the app with the same key of your own
(`apksigner sign --ks your-key.jks --out <name>-signed.apk <name>.apk`;
`zipalign` and `apksigner` are in the Android SDK's build-tools). Android
won't install an update signed with a different key, so uninstall SongShrink
first (this deletes its projects on that device) and install your APKs
together with `adb install-multiple`.

Duke & Duke checked the replace, `zipalign` and `apksigner` steps on a release
APK it built (the result signs and verifies, and stays 16 KB aligned); a copy
changed this way was not installed on a device. A copy you sign yourself isn't
updated by Google Play, and features that rely on Google Play (the purchase
and the vocal removal model download) may not work in it.

## How long this page stays up

Duke & Duke keeps this source, and these instructions, available here at no
charge for at least three years from the last date this version of LAME
ships in SongShrink.
