Live Activities 是狀態機,不是徽章

Return 中的 Live Activity 看起來像是鎖定畫面與動態島上的倒數數字。12但它不是一個數字。它是一台擁有五種生命週期狀態、三條外部解除路徑,以及一條必須自我防禦的可重入啟動路徑的狀態機。下方所述的模式是在生產環境中存活下來的那些。文末殘酷誠實的註腳會說明我尚未弄清楚的部分。

我曾推出一個 v1 版本,把 Live Activity 當成徽章來處理。「目前剩餘時間」是資料;其餘都是裝飾。那個版本有三個我在 TestFlight 抓到的錯誤,以及一個在生產環境才抓到的錯誤:

  1. 在啟動還在傳輸中時又點選啟動,會建立第二個活動,並讓第一個變成孤兒。
  2. 倒數計時在動態島上顯示正確,但鎖定畫面檢視在計時器暫停時會踩到 endTime <= Date(),於是顯示 0:00 直到使用者恢復計時為止。
  3. 使用者重設計時器後,Live Activity 仍長時間停留在畫面上,因為解除政策是 .default,而 Apple 會讓它顯示一段時間,最長可達四小時。
  4. (生產環境。)在從右至左語言區域(阿拉伯文、希伯來文)下,動態島緊湊尾端區域中的數字以反向呈現。拉丁數字搭配 RTL 版面。修正只需一行。

每一項都是狀態機的錯誤。倒數的數字本身沒問題。數字並不是產品。狀態才是產品。

下方的狀態機是從那些錯誤中存活下來的版本。

TL;DR

  • 上線版本的 LiveActivityManager 提供 5 個轉換方法(startActivity、updateActivity、showCycleComplete、showFinalCompletion、endActivity),加上 1 個讀取方法(hasActiveActivity)。這 224 行生產程式碼,在 startActivity 內部專門守住一個特定危險:並發的啟動呼叫,以及在該方法每個 await 邊界進行的取消檢查。3
  • ContentState 攜帶 6 個欄位:endTime、currentCycle、totalCycles、isPaused、isCompleted、remainingSeconds。前五個是狀態機的標籤。第六個(remainingSeconds)是 ActivityKit 的即時 timerInterval 無法服務時的靜態顯示備援。
  • 解除政策的決定才是真正的產品決策。使用者重設時用 .immediate,完成時用 .after(Date().addingTimeInterval(3)),絕不要用系統預設值。
  • 動態島緊湊尾端區域需要在計時器文字上加上 .environment(\.layoutDirection, .leftToRight),才能在 RTL 系統地區下保持拉丁數字 LTR。

狀態機

上線版本的 Live Activity 有一個閒置狀態、三個使用者可觀察的執行中狀態、一個終結狀態,以及一個開發者必須注意的可重入閘門:

┌──────────────────────────────────────────────────────────────────┐
│                  Lifecycle states                                 │
├──────────────────────────────────────────────────────────────────┤
│  IDLE          currentActivity == nil; no Live Activity present   │
│  RUNNING       isPaused=false, endTime > Date()                   │
│  PAUSED        isPaused=true, remainingSeconds=N                  │
│  CYCLE_END     isPaused=false, endTime <= Date(), isCompleted=false│
│  COMPLETE      isCompleted=true (terminal; transitions to IDLE)   │
└──────────────────────────────────────────────────────────────────┘
              │
              ↓
┌──────────────────────────────────────────────────────────────────┐
│             Dismissal policies (Apple)                            │
├──────────────────────────────────────────────────────────────────┤
│  .immediate            user reset                                  │
│  .after(now + 3s)      completion display window                   │
│  .default              system decides; can stay up to 4 hours      │
└──────────────────────────────────────────────────────────────────┘

Reentrancy gate inside startActivity():
  isStartingActivity flag + cancellable startActivityTask
  prevents two concurrent startActivity() calls from creating
  two Live Activities for one timer. Cancellation checks across
  each await keep the in-flight task safe to abort.

繪製路徑會先檢查 isPaused;正是這個順序讓暫停中的計時器在牆鐘時間越過 endTime 時也不會被繪製成 CYCLE_END。7

這些狀態名稱不是套在數字上的標籤。狀態名稱是 LiveActivityManager(應用端,我的 SwiftUI 檢視所在處)與 ReturnLiveActivity(Widget 擴充功能,由 Apple 程序負責繪製介面)之間的契約。

這份契約是 TimerActivityAttributes.ContentState,總共 6 個欄位:3

public struct ContentState: Codable, Hashable {
    var endTime: Date
    var currentCycle: Int
    var totalCycles: Int?
    var isPaused: Bool
    var isCompleted: Bool = false
    var remainingSeconds: Int = 0
}

每一次狀態轉換都會改寫這個結構,並要求 ActivityKit 將其跨程序邊界傳遞給 Widget 擴充功能。Widget 接著重新繪製。沒有共享記憶體,沒有回呼。有的是一個 Codable 結構,會在每次轉換時跨越程序邊界。

這個事實排除了我可能想用閉包、view model、observable object 或計算屬性來做的任何事情。狀態必須以可序列化的資料表示。如果無法編碼,就不能轉換。

可重入啟動

Live Activities 對並發活動有硬上限,對在傳輸途中重複呼叫 Activity.request 會發生什麼事則有軟上限。硬上限有完整文件記載。4軟上限是「第二次呼叫可能成功並建立一個孤兒。」這個孤兒就是不再與您的 manager 中 currentActivity 關聯的那個 Live Activity。它會繼續存在,沒有任何路徑能重新回到您的程式碼中。它最終會自行依過時計時器解除。在那之前,使用者會看到一個重複的計時器。

這個孤兒就是 Return 上線時的 v1 錯誤。修正方式是在 LiveActivityManager.swift 中加入可重入閘門加可取消的 Task:3

private var isStartingActivity = false
private var startActivityTask: Task<Void, Never>?

func startActivity(...) {
    #if os(iOS)
    guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
    guard !isStartingActivity else { return }
    isStartingActivity = true

    startActivityTask?.cancel()

    startActivityTask = Task {
        defer {
            isStartingActivity = false
            startActivityTask = nil
        }
        guard !Task.isCancelled else { return }

        await endActivity()  // explicit cleanup of any prior state

        guard !Task.isCancelled else { return }

        // ... build attributes + contentState ...

        do {
            let activity = try Activity.request(...)
            guard !Task.isCancelled else { return }
            currentActivity = activity
        } catch {
            // log; flag clears via defer
        }
    }
    #endif
}

關於這個模式有三件事文件中沒有特別點出:

isStartingActivity 旗標才是真正的主動防護;startActivityTask?.cancel() 是防禦性清理。這個旗標會在第一次呼叫還在傳輸中時短路任何第二次的 startActivity 呼叫,因此公開路徑上不會真正發生競爭。先取消再替換的舞步仍然重要,因為傳輸中的 Task 是非同步的,可能比短命的呼叫者活得更久;取消可以避免一個過時的 Task 在呼叫者已離開之後繼續執行。

每個 await 邊界都要做 guard !Task.isCancelled 檢查。Swift 的取消是合作式的。即使 cancel 已被呼叫,Task 仍會繼續執行,直到它顯式檢查為止。每個 await 都是一次檢查的機會。少了 await 後的檢查,被取消的 Task 會繼續構建活動狀態、呼叫 Activity.request,並在成功時悄悄建立一個孤兒。

defer 會在 Task 主體完成之前清除旗標。沒有 defer 的話,提早 return(來自取消檢查)會永久將 isStartingActivity = true 卡住,活動就再也不會啟動,直到應用重啟。這個旗標是一把鎖;鎖必須在每條離開路徑上釋放。

pushType: nil 引數。Return 不使用 APNs 推送的 Live Activity 更新。應用程式透過 activity.update 在本機更新活動。如果您需要由推送驅動的更新(送貨追蹤、運動賽事比分、即時資料),那麼類型為 pushType: .token,契約也會複雜得多。5本機更新較為簡單,並足以涵蓋任何計時器/計數器/單一應用程式的工作流程。

暫停問題

ActivityKit 提供了一個漂亮的 Text(timerInterval: Date()...endTime, countsDown: true) 檢視,能繪製即時倒數而不需要應用程式做任何更新。6您設定結束時間,系統就會繪製即時計時器。不需要 Timer.publish、不需要 widget 重新整理、不耗電。

當計時器在執行時,這非常棒。當計時器被暫停時,這就錯了。

無論狀態中是否有「暫停」訊號,timerInterval 文字都會持續朝 endTime 倒數。Apple 的 API 中沒有「凍結在 10:23」的模式。如果您傳入 endTime = Date().addingTimeInterval(623),使用者在 10:23 處暫停,計時器文字仍會在 widget 中持續倒數到零。狀態欄位顯示已暫停,widget 卻顯示在執行。

修正方式是從同一個狀態繪製兩種不同的檢視:7

if context.state.isPaused {
    // static text
    Text(formatTime(context.state.remainingSeconds))
        .monospacedDigit()
} else if context.state.endTime > Date() {
    // live countdown
    Text(timerInterval: Date()...context.state.endTime, countsDown: true)
        .monospacedDigit()
} else {
    // post-end static
    Text("0:00")
        .monospacedDigit()
}

雙軌繪製就是 ContentState 把 remainingSeconds 當作獨立欄位攜帶的原因。當計時器在執行時,它是冗餘的(系統會從 endTime 計算)。當計時器被暫停時,它是唯一的真實來源。這個結構的兩半服務於兩種不同的繪製模式;isPaused 布林值在兩者間做選擇。

解除政策

activity.end(_:dismissalPolicy:) 接受三種 ActivityUIDismissalPolicy 值之一,選錯就是讓我的 v1 在使用者重設後,仍然在鎖定畫面上停留得彷彿天荒地老的原因:13

政策 何時使用 您會得到什麼
.immediate 使用者重設、發生錯誤、應用程式進入背景且無活動需要追蹤 活動立即消失。沒有寬限期
.after(date) 完成顯示:「您的冥想已完成」需要被閱讀片刻。日期必須在 Apple 允許的四小時窗口內 活動顯示最終狀態,然後在 date 解除
.default 當您真心想讓 Apple 的啟發式邏輯來決定時 系統會讓它「顯示一段時間」(Apple 用語),在 end 被呼叫後最長可達四小時

Return 在自然完成路徑上使用 .after(Date().addingTimeInterval(3)):3

await activity.end(
    .init(state: contentState, staleDate: nil),
    dismissalPolicy: .after(Date().addingTimeInterval(3))
)

三秒是使用者瞄一眼鎖定畫面、意識到計時器結束、感受到打勾符號帶來的滿足感所需要的時間。少於三秒會顯得跳動。多於三秒會讓人感覺活動不知道自己已經結束了。

對於使用者觸發的重設,呼叫是 dismissalPolicy: .immediate。沒有窗口。使用者已經知道了。

v1 的錯誤選擇是 .default。對於完成的冥想計時器,系統會把活動保留可見足夠久,使用者會以為應用程式根本沒有偵測到完成。Apple 文件指出 .default 會讓已結束的活動「顯示一段時間」,最長可達四小時;13計時器的正確做法是讓解除明確化。

動態島緊湊區域

動態島有三種繪製模式,即使是簡單的計時器,這三種您都需要:2

  • 緊湊(動態島預設形狀):前端圖示加上尾端計時器
  • 最小(當另一個 Live Activity 競爭同一個動態島時):僅顯示前端圖示
  • 展開(長按):四個具名區域(leading、trailing、center、bottom)

在 Return 中贏得一席之地的模式是讓展開檢視幾乎與緊湊檢視相同:8

DynamicIsland {
    DynamicIslandExpandedRegion(.leading) {
        Image("AppIconSmall")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 16, height: 16)
            .clipShape(RoundedRectangle(cornerRadius: 4))
    }
    DynamicIslandExpandedRegion(.trailing) {
        TimerText(...)
    }
    DynamicIslandExpandedRegion(.center) { EmptyView() }
    DynamicIslandExpandedRegion(.bottom) { EmptyView() }
} compactLeading: {
    Image("AppIconSmall")...
} compactTrailing: {
    TimerText(...)
} minimal: {
    Image("AppIconSmall")...
}

大多數 Live Activity 教學會把展開檢視當作「真正的」設計,並在 bottom 區域放豐富內容。對於冥想計時器來說,展開是無謂的累贅。使用者透過長按打開展開檢視,而長按本身已經提供了「有事發生」的觸覺回饋。加入內容反而會讓展開說出使用者沒要求的話。展開模式中的空區域並非設計失敗,它們本身就是設計。

RTL 錯誤

那個生產環境的錯誤。iOS 上的阿拉伯文與希伯來文使用者回報,動態島緊湊尾端的計時器數字以反向呈現。拉丁數字字串 5:23 被繪製成 32:5,因為緊湊尾端的版面方向繼承了系統地區的 RTL 設定。

SwiftUI 在 widget 程序內會繼承系統的版面方向,因此當使用者的手機設定為阿拉伯文或希伯來文時,動態島的計時器文字也會跟著套用 RTL。即使在 RTL 介面中,拉丁數字也應該以 LTR 繪製。修正方式是在數字文字檢視上鎖定版面方向:7

.environment(\.layoutDirection, .leftToRight)

這個覆寫應加在 TimerText 內的數字 Text 檢視(動態島緊湊/展開)以及鎖定畫面檢視內,而不是整個檢視。拉丁數字無論使用者的系統地區為何,都應該由左至右閱讀;像「Cycle 2 of 3」這類的循環標籤則保持本地化,因此會跟隨系統的版面方向。

這個錯誤在本國地區的 TestFlight 中不會浮現。它會在真實的 RTL 使用者打開計時器的那一刻浮現。教訓:在任何可能在 RTL 地區執行的 Live Activity 中,每一個拉丁數字文字檢視都要部署 LTR 鎖定的環境覆寫。

本地化的故事

TimerActivityAttributes 會攜帶一個 languageCode: String 欄位,由應用程式在建立活動時設定:9

let attributes = TimerActivityAttributes(
    timerDuration: duration,
    languageCode: settings.appLanguage  // app's selected language, not system's
)

Widget 擴充功能會讀取這個欄位來繪製本地化字串:

private var locale: Locale {
    let code = context.attributes.languageCode
    return code.isEmpty ? .current : Locale(identifier: code)
}

private func localized(_ key: String.LocalizationValue) -> String {
    String(localized: key, locale: locale)
}

為什麼應用程式要傳入自己的語言代碼,而不是讓 widget 直接讀取 Locale.current:因為 widget 擴充功能執行於自己的程序中。它的 Locale.current 是系統地區,不是應用程式選擇的地區。如果使用者把 Return 設為韓文,但 iPhone 是英文,沒有這個覆寫的話,widget 就會說英文。應用程式的語言偏好會透過活動屬性一同傳遞;widget 會予以尊重。

Localizable.xcstrings 與應用程式並存於 widget 目標中,但它們是獨立的檔案。即使同一個字串已存在於 Return/Localizable.xcstrings,widget 中使用的字串仍必須存在於 ReturnWidgets/Localizable.xcstrings。忘記這點意味著當應用程式說韓文時,widget 會回退到開發語言。

我會如何重新打造

讓 ContentState 變小。六個欄位太多了。endTime 與 remainingSeconds 之間的冗餘,是繞過 timerInterval 沒有暫停模式所付出的代價。如果重新開始,我會攜帶單一的 displayMode 列舉(running、paused(remainingSeconds: Int)、cycleEnd、complete),讓繪製程式碼依 case 分派。六個欄位要在五個轉換方法之間維持正確改寫,比四個 case 困難。

加入互動式 Live Activity 按鈕(iOS 17+)。Return 目前並未在動態島中提供暫停/恢復控制項。使用者必須打開應用程式才能暫停。iOS 17 為 Live Activities 內的 App Intents 加入了 Button(intent:)。10互動式暫停控制是顯而易見的擴充,也是我接下來會為 Return 推出的下一項。

用推送更新的 Live Activities 做跨裝置計時器同步。Return 透過 NSUbiquitousKeyValueStore 在 iPhone、iPad、Watch 與 Apple TV 之間同步工作階段(在 Five Apple Platforms, Three Shared Files 中詳述)。今天活動是從 iPhone 或 iPad 應用程式本機啟動並本機更新。理想情況下,使用者在 Apple Watch 上啟動計時器時,能在 iPhone 上即時看到 Live Activity 反映出該狀態。透過 APNs 推送至 Live Activity 就是路徑。5我還沒做。

何時不要使用 Live Activities

一次性的暫態狀態。「已儲存!」這類的 Toast 不值得用 Live Activity。系統已經有橫幅。請用它。

沒有時間維度的頻繁變動資料。Live Activities 最適合具有清晰時間錨點的事物(計時器、送貨 ETA、比賽計時、通話時間)。股價跑馬燈與運動賽事比分能用,是因為它們有一個工作階段窗口。通用的儀表板則不行。

沒有鎖定畫面/待機使用情境的應用程式。Live Activities 需要實實在在的工程投入(目標設定、ContentState 設計、解除政策決定、RTL 處理、本地化管線)。使用者直接打開應用、使用過程中從未查看過鎖定畫面的應用,並不是合適的形狀。相片編輯器不需要。健身追蹤器需要。

在非 iOS 介面上,但有附帶條件。Return 的 LiveActivityManager 將實作放在 #if os(iOS) 之後,因為計時器是從 iPhone 或 iPad 應用啟動的。ActivityKit 本身將鎖定畫面橫幅、動態島、Apple Watch Smart Stack、Mac 與 CarPlay 描述為呈現介面;iOS 26 擴充了其中數個。4watchOS 仍有自己的 complications API 用於全螢幕繪製。macOS 有選單列應用。iPadOS 自 iPadOS 17 起支援 Live Activities,但沒有動態島區域。Return 的 manager 在一個 224 行的檔案中有 8 個 #if os(iOS) 守衛。

對於在 iOS 26+ 上推出的應用,這個模式意味著什麼

兩個重點。

  1. 把 Live Activity 當作狀態機,不是數字。狀態機有清楚的狀態、清楚的轉換與清楚的解除規則。畫面上的數字只是某個狀態的一種繪製。先把狀態做對。

  2. 可重入守衛是您還沒踩到的那個錯誤。我在實際環境中看過的每一個沒有實作 isStartingActivity 加可取消 Task 的 Live Activity manager,至少都出過一次孤兒活動的錯誤。這個守衛只有 6 行。寫一次就好。

請將這篇與我為同一系列應用所寫的文章一起閱讀:給 Apple Intelligence 用的型別化 App Intents;用於跨 LLM 代理的 MCP 伺服器;視覺層的 Liquid Glass 模式;跨裝置觸及的多平台推出。Live Activities 是同一個堆疊中的 iOS 鎖定畫面與動態島層。完整的系列在 Apple Ecosystem Series 中樞。要了解更廣泛的 iOS 加 AI 代理脈絡,請參閱 iOS Agent Development 指南。

FAQ

Live Activities 與 WidgetKit widget 有何不同?

WidgetKit widget 是依 TimelineProvider 定義的間隔繪製;系統決定何時重新整理,widget 從靜態時間軸重新繪製。11Live Activities 則是回應特定應用程式驅動的 activity.update(...) 呼叫而繪製,並在底層活動的整個期間(計時器、送貨、健身)持續存在。兩者都打包在 widget 擴充功能目標中;差別在觸發模型。

Live Activities 在 iPad 上能用嗎?

可以,從 iPadOS 17+ 起。鎖定畫面橫幅是主要的繪製介面;iPad 沒有動態島。同樣的 ActivityConfiguration 程式碼可用;只要預期動態島區域永遠不會在 iPad 上繪製即可。

Live Activity 能在我的應用程式程序之外存活嗎?

可以。一旦 Activity.request 成功,活動就由 ActivityKit 擁有。應用程式程序可被系統終止;活動會繼續在鎖定畫面與動態島上繪製,直到您明確結束它(或直到系統的過時規則將其解除)。明確的 endActivity() 呼叫之所以重要正是這個原因;若應用程式重設時沒有明確結束,活動就會比計時器活得更久。

為什麼這篇文章沒有涵蓋推送更新的 Live Activities?

我尚未在 Return 中推出推送更新的 Live Activities。依本系列的類型規則:上線程式碼類文章只記錄生產程式碼實際做的事。推送更新已列在「我會如何重新打造」中;等我推出之後,會有一篇文章專門涵蓋。

SwiftUI 應用中 Live Activities 的實際檔案配置是什麼?

三個部分:3712

  • 在主應用目標中:LiveActivityManager.swift(管理活動生命週期)、TimerActivityAttributes.swift(與 widget 共享的 ActivityAttributes 結構;兩個目標都會編譯這個檔案)。
  • 在 widget 擴充功能目標中:ReturnLiveActivity.swift(具備 ActivityConfiguration 主體的 Widget 一致性)、ReturnWidgetsBundle.swift(@main WidgetBundle)。
  • 設定:在應用目標中加入 Info.plist,並設定 NSSupportsLiveActivities = YES。

Widget 擴充功能目標需要 ActivityKit 與 WidgetKit 匯入。TimerActivityAttributes 是兩個目標之間唯一共享的檔案;其餘都是目標隔離的。


Live Activity 不是鎖定畫面上的一個數字。它是一台每次轉換都會跨越程序邊界的狀態機。把狀態做對、守住可重入、刻意選擇解除政策、釘住版面方向。數字會自己照顧好自己。

References


  1. Author’s Return, a SwiftUI meditation timer published on the App Store on April 21, 2026, available for iPhone, iPad, Mac, Apple Watch, and Apple TV. Live Activities ship on the iOS target only. ↩

  2. Apple Developer, “ActivityKit framework”. Lock Screen banner, Dynamic Island compact / minimal / expanded modes, activity lifecycle. Available iOS 16.1+; Dynamic Island available iPhone 14 Pro and later. ↩↩

  3. Production code in Return/Return/LiveActivityManager.swift (224 lines, 8 #if os(iOS) blocks) and Return/Return/TimerActivityAttributes.swift (43 lines). Shared between the app target and the widget extension target via target membership. ↩↩↩↩↩

  4. Apple Developer, “Displaying live data with Live Activities”. Concurrency limits, supported platforms (iOS 16.1+, iPadOS 17+), NSSupportsLiveActivities Info.plist key. ↩↩

  5. Apple Developer, “Updating and ending your Live Activity with ActivityKit push notifications”. The pushType: .token path requires a separate APNs auth key, server-side push token registration, and a different update protocol from local activity.update(...) calls. ↩↩

  6. Apple Developer, “Text(timerInterval:pauseTime:countsDown:showsHours:)”. Live system-rendered countdown timer; renders without app updates while the activity is running. ↩

  7. Production code in Return/ReturnWidgets/ReturnLiveActivity.swift (232 lines). The widget extension’s Widget conformance with ActivityConfiguration<TimerActivityAttributes> body. The TimerText view at lines 61-102 handles the paused / running / post-end three-state rendering. ↩↩↩↩

  8. Apple Developer, “DynamicIsland”. The four named expanded regions (leading, trailing, center, bottom) plus three compact-mode views (compactLeading, compactTrailing, minimal). ↩

  9. The widget extension runs in its own process and inherits the system locale, not the app’s selected locale. Apps that support in-app language switching (Return supports 27 languages) must pass the language code through ActivityAttributes so the widget can render in the user’s chosen language. Pattern: Locale(identifier: context.attributes.languageCode) rather than Locale.current. ↩

  10. Apple Developer, “Button(intent:)”. Available in widget and Live Activity views from iOS 17+. Bridges App Intents into Lock Screen / Dynamic Island controls without requiring app foregrounding. ↩

  11. Apple Developer, “TimelineProvider”. The widget refresh model that predates Live Activities; pre-computed entries with system-managed reload windows. ↩

  12. Production code in Return/ReturnWidgets/ReturnWidgetsBundle.swift (16 lines). The @main WidgetBundle that registers ReturnLiveActivity as the widget extension’s only widget. Required pattern for widget extensions; the bundle is what the system loads. ↩

  13. Apple Developer, “ActivityUIDismissalPolicy”. Three cases: .default, .immediate, .after(_:). Apple states .default keeps an ended Live Activity visible “for some time” up to four hours, and .after(_:) accepts a date within the same four-hour window. ↩↩

相關文章

iOS 26 的小工具介面:一個 App Intent,多處呈現

iOS 26 的小工具、控制中心控制項與即時動態,全都是 App Intents 的呈現面。一個 intent 即可驅動按鈕、控制項與即時動態的操作。

8 分鐘閱讀

iOS 26 上的 HealthKit + SwiftUI:授權、樣本類型,以及兩款上架 App 的跨平台模式

來自 Water(飲水追蹤、HKQuantitySample)與 Return(正念冥想、HKCategorySample)的真實生產級模式。權限體驗、async 包裝、watchOS 變體,以及務必避開的陷阱。

12 分鐘閱讀

Still Rooms:AI 作曲,而非 AI 生成

Return 裡的那張專輯 Still Rooms,如何由模型寫成樂譜、以有名字的樂器渲染、用無損格式發行,並在 App 內以 5.1 聲道播放。

26 分鐘閱讀