Macが書き出すm3uをWALKMAN互換へ変換する方法(2026年7月24日更新)

※旧タイトル:MacのプレイリストをWALKMAN NW-A306で再生する方法【完全解決】

Macのミュージックアプリ(旧iTunes)で作成したプレイリストをそのままSONY WALKMAN NW-A306(Android) に転送して再生しようとコピーしてみましたが、再生どころかプレイリスト自体が表示されません。

調べてみるとAppleとSONYではプレイリストの仕様が複数違っており、それぞれ変換する必要がありました。

Macから書き出したプレイリストは見事に絶対パスでした
Macから書き出したプレイリストは見事に絶対パスでした

原因は単純ではなく、Apple独自のプレイリスト仕様とWindows環境、さらにWALKMAN側が期待する.m3u8仕様がかみ合っていないためです。

当初は「プレイリストのパスを書き換えるだけ」の小さなPowerShellスクリプトとして書き始めました。しかし動かしては失敗を繰り返すうちに、PowerShell 7専用APIへの依存、Unicode正規化、URLエンコード、ローマ数字など、実用面では無視できない問題が次々と見つかりました。

結果として、スクリプトは全部書き直すことになり、設計から変え、どうせならWindows PowerShell 5.1とPowerShell 7の両方で動作することを前提とした方向で勉強しなおしました。

この記事ではコードの解説だけではなく、なぜその設計へ変更しなければならなかったかを中心にまとめます。

結論

絶対に直さなければならなかった点と改善した内容はほぼ同じなので、改善したところと変更内容をまとめました。

  • macOSの絶対パスをWALKMAN用の相対パスへ変換
  • %20などURLエンコードを復元
  • Unicode NFDをNFCへ正規化
  • Unicodeローマ数字を通常ASCIIへ変換
  • UTF-8(BOM付き)の.m3u8として保存
改善点変更点
PowerShell互換性5.1と7の両対応
パス抽出固定文字列から正規表現へ変更
プレイリスト絶対パスから相対パスへ変更
URLエンコード%20などを元の文字へ復元
UnicodeNFDをNFCへ正規化
ローマ数字Unicode装飾文字をASCIIへ変換
安全性DryRun、ShouldProcess、例外処理を追加
運用性入力チェックとサマリーを表示してみた

やっていることはほぼ同じなのですが、新しく作り直したことで中身はほぼ別物です。

こちらが書き直したスクリプトです。(2026年7月24日)

[strings]を書き換えることで汎用性が高まるかなと思います。

[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]

# [strings]は環境に合わせて変更してください。
param(
    [string]$PlaylistDir      = "E:\Music\Playlists",
    [string]$MacRootRegex     = '^/Users/[^/]+/Music/Music/Media\.localized/Music/',
    [string]$WalkmanMusicRoot = "E:\Artist",
    [string]$WalkmanPlaylists = "E:\Music\Playlists",
    [switch]$DryRun,
    [switch]$Force
)

$ErrorActionPreference = 'Stop'

try {
    $MacRootRegexObj = [regex]::new($MacRootRegex)
}
catch {
    throw "MacRootRegex が正しい表現ではありません: $MacRootRegex ($($_.Exception.Message))"
}

$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)

# --- 装飾ローマ数字をASCIIへ ---

$RomanMap = [System.Collections.Specialized.OrderedDictionary]::new([StringComparer]::Ordinal)
$RomanPairs = @(
    @("Ⅰ","I"), @("Ⅱ","II"), @("Ⅲ","III"), @("Ⅳ","IV"), @("Ⅴ","V"), @("Ⅵ","VI"),
    @("Ⅶ","VII"), @("Ⅷ","VIII"), @("Ⅸ","IX"), @("Ⅹ","X"), @("Ⅺ","XI"), @("Ⅻ","XII"),
    @("Ⅼ","L"), @("Ⅽ","C"), @("Ⅾ","D"), @("Ⅿ","M"),
    @("ⅰ","i"), @("ⅱ","ii"), @("ⅲ","iii"), @("ⅳ","iv"), @("ⅴ","v"), @("ⅵ","vi"),
    @("ⅶ","vii"), @("ⅷ","viii"), @("ⅸ","ix"), @("ⅹ","x"), @("ⅺ","xi"), @("ⅻ","xii"),
    @("ⅼ","l"), @("ⅽ","c"), @("ⅾ","d"), @("ⅿ","m")
)
foreach ($pair in $RomanPairs) { $RomanMap.Add($pair[0], $pair[1]) }

function Convert-RomanToAscii {

    param([string]$Text)
    if ([string]::IsNullOrEmpty($Text)) { return $Text }
    foreach ($k in $RomanMap.Keys) {
        if ($Text.Contains($k)) {
            $Text = $Text.Replace($k, $RomanMap[$k])
        }
    }
    return $Text
}

function Normalize-Text {
    param([string]$Text)
    if ([string]::IsNullOrEmpty($Text)) { return $Text }
    $normalized = $Text.Normalize([Text.NormalizationForm]::FormC)
    $normalized = Convert-RomanToAscii $normalized
    return $normalized
}

function Convert-UrlDecode {

    param([string]$Text)
    return [Uri]::UnescapeDataString($Text)
}

function Get-RelativePathCompat {

    param(
        [string]$From,
        [string]$To
    )
    $fromUri = [Uri]::new(($From.TrimEnd('\') + '\'))
    $toUri   = [Uri]::new($To)
    $relUri  = $fromUri.MakeRelativeUri($toUri)
    $rel     = [Uri]::UnescapeDataString($relUri.ToString())
    return ($rel -replace '/', '\')
}

# --- 事前チェック ---

if (-not (Test-Path -LiteralPath $PlaylistDir)) {
    throw "プレイリストフォルダが見つかりません: $PlaylistDir"
}

if (-not (Test-Path -LiteralPath $WalkmanPlaylists)) {

    if ($DryRun) {
        Write-Host "  [DryRun] フォルダ作成予定地: $WalkmanPlaylists"
    }
    elseif ($PSCmdlet.ShouldProcess($WalkmanPlaylists, "フォルダ作成")) {
        New-Item -ItemType Directory -Path $WalkmanPlaylists -Force | Out-Null
    }
}

$m3uFiles = Get-ChildItem -LiteralPath $PlaylistDir -Filter *.m3u -File
if (-not $m3uFiles) {
    Write-Warning "m3u ファイルが見つかりません: $PlaylistDir"
    return
}

Write-Host "=== 変換する対象は: $($m3uFiles.Count) 件です ==="
$m3uFiles | ForEach-Object { Write-Host "  - $($_.Name)" }

# --- 実行前の確認 (y/n) ---

if (-not $Force -and -not $DryRun) {
    $answer = Read-Host "`nこれらのファイルを変換し、'$WalkmanPlaylists' に書き込みます。よろしいですか? (y/n)"
    if ($answer -notin @('y', 'Y', 'yes', 'Yes')) {
        Write-Host "中断します。ファイルは変更していません。"
        return
    }
}

$summary = [System.Collections.Generic.List[string]]::new()

foreach ($m3u in $m3uFiles) {
    try {
        $inputM3U   = $m3u.FullName
        $baseName   = $m3u.BaseName
        $outputM3U8 = Join-Path $WalkmanPlaylists ($baseName + ".m3u8")

        # --- 既にあるファイルの上書き確認 (y/n) ---

        if ((Test-Path -LiteralPath $outputM3U8) -and -not $Force -and -not $DryRun) {
            $ow = Read-Host "  '$($baseName).m3u8' は既に存在しています。上書き保存をしますか? (y/n)"
            if ($ow -notin @('y', 'Y', 'yes', 'Yes')) {
                Write-Host "  スキップ: $($m3u.Name)"
                continue
            }
        }

        Write-Host "=== 変換中: $($m3u.Name) -> $($baseName).m3u8 ==="

        # 日本語ファイル名を含むm3uはUTF-8(BOM付き)を前提。
        $lines = Get-Content -LiteralPath $inputM3U -Encoding UTF8
        $out   = [System.Collections.Generic.List[string]]::new()
        $unmatched = 0

        foreach ($line in $lines) {

            if ($line.StartsWith("#") -or [string]::IsNullOrWhiteSpace($line)) {
                $out.Add($line)
                continue
            }

            $work = $line.Trim()

            # ユーザー名や私の環境の "Music/Music" 二重フォルダの揺れ対策のために正規表現でマッチ
            $match = $MacRootRegexObj.Match($work)

            if ($match.Success) {
                $relative = $work.Substring($match.Index + $match.Length)
                $relative = $relative -replace "/", "\"
                $relative = Convert-UrlDecode $relative
                $relative = Normalize-Text $relative

                $walkmanAbs = Join-Path $WalkmanMusicRoot $relative
                $relativeForPlaylist = Get-RelativePathCompat -From $WalkmanPlaylists -To $walkmanAbs

                if (-not (Test-Path -LiteralPath $walkmanAbs)) {
                    Write-Warning "  実体ファイルなし: $relative"
                }

                $out.Add($relativeForPlaylist)
            }
            else {
                # ルートに一致しない行は変換されずそのまま残る。壊れたパスになるかもしれないです。
                $unmatched++
                Write-Warning "  ルート不一致(未変換): $work"
                $out.Add((Normalize-Text $work))
            }
        }

        if ($DryRun) {
            Write-Host "  [DryRun] $($out.Count) 行を出力予定。書き込みはしません。"
        }
        elseif ($PSCmdlet.ShouldProcess($outputM3U8, "書き込み")) {
            [System.IO.File]::WriteAllLines($outputM3U8, $out, $Utf8NoBom)
            Write-Host "  -> 出力: $outputM3U8"
        }

        $summary.Add("$($m3u.Name): 総行数 $($lines.Count) / 未変換 $unmatched 件です")
    }
    catch {
        Write-Warning "  エラー: $($m3u.Name) の処理中に失敗しました - $($_.Exception.Message)"
        $summary.Add("$($m3u.Name): エラーで中断")
    }
}

Write-Host "`n=== 完了 ==="
$summary | ForEach-Object { Write-Host "  $_" }

検証環境

次の環境で検証をしました。

  • macOS ミュージック.appで出力したプレイリスト(m3u)
  • ミュージックライブラリ
  • Windows 11
  • Windows PowerShell 5.1
  • PowerShell 7
  • SONY WALKMAN NW-A306

目的は、Macで管理しているプレイリストをWindows経由でWALKMANでも使いまわせるようにするということです。

アルバムアートが表示されない
プレイリストがない、アルバムアートが表示されない

なぜWALKMANでは読み込めないのか

ここが結構闇が深く、調べるだけでどんどん気力がそがれるような感覚に陥りました。

macOSで出力したプレイリストには次のように書かれています。

  • macOSの絶対パス
  • URLエンコード
  • Unicode NFD
  • Apple独自のフォルダ構造

これらがそのまま記録されていました。

一方WALKMANが欲しいものはWindows形式の相対パスで保存された.m3u8です。

つまり、プレイリストを書き換えるのではなく、Apple仕様をWALKMAN仕様へ変換する処理が必要でした。

設計変更 1:PowerShell 5.1でも動くようにした

今回もっとも大きな変更点です。前回のスクリプトはPowerShell 7でしか動きませんでした。しかし、このブログでは誰でも再現できるようにしたいと思っているため、この記事だけインストールが必要なPowerShell 7を使っていることに違和感を感じていました。

PowerShell 7を使っていた理由はいくつかありますが、一番は次のものです。

System.IO.Path::GetRelativePath()

これは.NET Core以降で追加されたAPIであり、Windows PowerShell 5.1(.NET Framework)には存在しません。

そこで相対パス生成をPowerShell 5.1でも使えるようにしました。

Uri.MakeRelativeUri()

このAPIは.NET Framework時代から存在しているため、PowerShell 5.1でもPowerShell 7でも同じ結果を得られます。

長く使うAPIを選ぶことはブログのテーマとしては正しいのでしょうが、実際問題新しいAPIを導入した方がいいとは思っています。

設計変更 2:固定文字列検索をやめて正規表現へ

初期版ではあまり考えずに/Users/username/という固定文字列で検索していました。

しかし実際にはユーザー名は環境ごとに異なります。そこで固定文字列の比較をやめて次のように正規表現へ変更しました。

^/Users/[^/]+/Music/Music/Media\.localized/Music/

これによりユーザ名の違いをなんとかすることができるようになり、環境依存が大幅に減ったと思います。

設計変更 3:Unicodeを正しく扱う

macOSの.m3uには濁点付きの日本語がNFD形式で保存されています。一方Windowsでは通常NFCが使われています。そのままではうまくいきません。「が」と「か+゛」別文字です。

そこですべてNFCへ正規化してから処理をするように変更しました。

見た目は同じ文字でも内部コードが異なれば違う文字です。この処理を行わないとファイルが見つからないケースがあります。

設計変更 4:URLエンコードを復元する

macOSの.m3uではスペースなどが%20として保存されていました。

前回は.NET Frameworkの HttpUtility.UrlDecode() を使用していました。しかし、互換性を考えて次のように変更しました。

これによりPowerShell 5.1 / 7のどちらでも同じ処理を行うことができるようになりました。

設計変更 5:Unicodeローマ数字への対応

クラシック音楽では当たり前のようにⅣ、Ⅸ、ⅫといったUnicodeローマ数字が使われています。初期版では12種類程度で疲れてしまい、それ以降には対応しませんでした。

今回は気合をいれなおして、大文字、小文字、Unicode装飾ローマ数字、ここまで対応させました。

さらにPowerShellのハッシュテーブルでは「Ⅰ」と「ⅰ」が同一のものとして扱われる問題があります。そこでOrderedDictionaryとStringComparer.Ordinalを使用し、Unicodeのコードポイント単位で比較できるよう設計を変形しています。

見た目ではわからないところですが、動作の安定にはなかなかに重要なところだったのではないかと思っています。

設計変更 6:動くより安全に働くを意識した

初期版は実行すると即変換を開始していました。今回の書き直しでは次の要素をいれて見ました。

  • DryRun
  • ShouldProcess
  • Force
  • 入力チェック
  • 上書き確認
  • try / catch
  • エラーサマリー

意味合いとしては、誤って大量のプレイリストを上書きしてしまうリスクを減らすこと、失敗したファイルだけを確認できるようにしたこと、そのような感じで付け加えました。

このおかげというかせいというかで、スクリプトの規模は大きくなりましたが、ブログテーマである安全のことを考えると付けるべき改善だったと思います。

変更前と変更後

今回は、この記事だけPowerShell 7が必要だということが気になっていたため、PowerShell 5.1でも動くように前回のスクリプトから大幅に設計段階から丸ごと変えました。内容は次の表のとおりです。

項目前回(2025年4月21日)最新版(2026年7月24日)
PowerShell 5.1×
PowerShell 7
相対パス生成GetRelativePathMakeRelativeUri
URLデコードHttpUtilityUri
ローマ数字一部のみUnicode全対応
入力チェックなしあり
DryRunなしあり
例外処理なしあり
処理結果表示なしあり

もはや前回の原型をとどめていないスクリプトになりました。

結果

最終的に、macOSで管理しているプレイリストをWindows PowerShell 5.1 / PowerShell 7のどちらでも変換できるようになりました。

WALKMAN側でもプレイリストが正常に認識され、クラシック音楽で多用されるローマ数字や日本語ファイル名も問題なく再生できています。

単純な文字列置き換えスクリプトから始まりましたが、いろいろ試行錯誤して互換性を求めた結果、保守性や安全性も付け加えることができ、進化した設計になったのではないかと思います。

よくある質問(FAQ)

Q
PowerShell 7だけならもっと簡単に書けると思うのですが?
A

はい。GetRelativePath()など便利なAPIが使えるため、コード量はかなり減らすことができます。しかし、あくまでWindows標準機能だけで完結させたいため、PowerShell 5.1と互換性がある書き方をしています。

Q
なぜPowerShell 5.1へ対応したのですか?
A

Windowsには現在もPowerShell 5.1が標準搭載され、PowerShell 7はインストールする必要があります。ブログのテーマ上、「あるものでなんとかする」ため、PowerShell 5.1を利用することを決めました。

Q
このスクリプトはmacOSのミュージック.app専用ですか?
A

ミュージック.appから書き出した.m3uプレイリストを前提としていますが、同じような形式であれば応用はできると思います。

まとめ

今回は完全リライトという形となり、いろいろ変わりました。一番変わったのはコードの長さではありません。

前回の「動けばいい」という雑な方法から「仕様が変わるまで使える」ように設計自体を変更したことです。

この記事で紹介した変換スクリプトは、単にm3uをm3u8へ変換するだけではありません。

macOSのミュージック.appとWALKMANのプレイリストの仕様差を一つずつ検証し、PowerShell 5.1でも動作するよう設計を見直しながら改善を重ねた結果です。

同じような文字コードやパス変換の問題に直面したとき、この記事の考え方が別のスクリプト設計にも役立てば幸いです。

macOSのm3uの状態、絶対パスやローマ数字がたくさんあります
スクリプトで変換後のため色々変わっています

リファレンス

Microsoft公式:
Windows PowerShell 5.1 Documentation
https://learn.microsoft.com/powershell/scripting/

about_Functions
https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_functions

ShouldProcess (SupportsShouldProcess)
https://learn.microsoft.com/powershell/scripting/developer/cmdlet/requesting-confirmation-from-cmdlets

String.Normalize()…NFDからNFCの正規化
https://learn.microsoft.com/dotnet/api/system.string.normalize

NormalizationForm…Unicode正規化形式(FormC / FormD)関係
https://learn.microsoft.com/dotnet/api/system.text.normalizationform

Uri.UnescapeDataString()…PowerShell 5.1 / 7共通で%20や日本語URLエンコードを戻す処理関係
https://learn.microsoft.com/ja-jp/dotnet/api/system.uri.unescapedatastring?view=net-10.0

Uri.MakeRelativeUri()…Path.GetRelativePathの代替
https://learn.microsoft.com/dotnet/api/system.uri.makerelativeuri

Path.GetRelativePath()…PowerShell 7(.NET Core)専用APIとして比較できる
https://learn.microsoft.com/dotnet/api/system.io.path.getrelativepath

Unicode Normalization Forms…NFD / NFCの正式仕様
https://www.unicode.org/reports/tr15/

Unicode Character Database…ローマ数字(Ⅰ〜Ⅻ)や結合文字の仕様を調べる際の資料
https://www.unicode.org/ucd/

更新履歴

  • 2026年7月24日
  • 2025年4月21日
P
P

【この記事を書いた人】

黒瓜 勉(Lonely Lab運営):

PC-9821時代からのインターネット老人。英文学博士課程前期で培った英文一次ソースの解読を武器に、PC-98からWindows 3.1、そして現代のRaspberry Piまで、世代を超えた実機検証を行っている。

「今あるものを、もっと長く楽しむ」をテーマに、技術の流行り廃りに左右されない公式仕様に基づいた堅牢な環境構築を追求。実機検証による第一に安全性、第二に可能性、第三に再現性を担保した技術情報を発信中。

※ 本記事は実機環境で検証した内容をもとに執筆しています。動作環境やソフトウェアのバージョンが異なる場合は結果が変わる可能性があります。

詳しいプロフィール・運営方針はこちら
https://lonely1.jp/helloworld/

コメント