클라우드 Mac에서 iOS WAV 내보내기 검증: 프레임 수와 채널 순서

클라우드 Mac에서 iOS WAV 내보내기 검증: 프레임 수와 채널 순서

내보낸 WAV 파일이 재생은 되지만 끝부분이 잘려 있거나, 양쪽 채널에서 모두 소리가 나지만 좌우가 뒤바뀌는 경우가 있습니다. 이런 문제는 원격 데스크톱으로 직접 들어 보며 원인을 찾기 어렵습니다. 클라우드 Mac에서 먼저 고정된 테스트 신호로 ‘파일 쓰기, 닫기, 다시 읽기, 비교’의 전 과정을 검증한 다음, 같은 검증 조건을 iOS 내보내기 코드에 적용하면 결과를 더 쉽게 재현할 수 있습니다.

먼저 파일의 요구 조건 정의하기

이 예제에서는 리샘플링이나 게인 조정이 없는 부동소수점 PCM 경로만 검증합니다. 샘플 레이트는 48000Hz, 채널 수는 2개, 길이는 4800프레임으로, 재생 시간은 0.1초입니다. 왼쪽 채널의 샘플 값은 0.25, 오른쪽 채널은 -0.5로 고정합니다. 두 채널의 값이 달라야 채널 뒤바뀜, 믹싱, 모노 신호 복제를 구별할 수 있습니다.

오디오의 한 프레임에는 같은 시점의 모든 채널 샘플이 들어 있습니다. 따라서 이 예제의 샘플 수는 두 채널을 합쳐 4800개가 아니라, 채널마다 4800개입니다. 파일 크기는 헤더, 추가 청크, 인코딩 형식에 따라 달라지므로 바이트 수를 프레임 수 대신 사용할 수 없습니다.

이 절차는 스피커에 의존하지 않는 파일 수준의 검증입니다. 재생 라우팅, Bluetooth 장치, 실제 기기의 녹음 기능이 정상이라는 뜻은 아닙니다. 고정된 직류 신호는 수치 검사용이며, 청취용이 아닙니다.

클라우드 Mac에서 최소 검증 절차 실행하기

먼저 xcrun --find swift를 실행해 현재 개발 도구에서 Swift를 찾을 수 있는지 확인합니다. 아래 코드를 wav-check.swift로 저장한 뒤, 쓰기 권한이 있는 디렉터리에서 xcrun swift wav-check.swift를 실행합니다. 스크립트는 자체 임시 디렉터리를 생성하므로 기존 파일을 덮어쓰지 않습니다. 검증 조건을 만족하지 못하면 0이 아닌 종료 코드로 종료됩니다.

import Foundation
import AVFoundation

let root = FileManager.default.temporaryDirectory
    .appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(
    at: root, withIntermediateDirectories: true, attributes: nil
)
defer { try? FileManager.default.removeItem(at: root) }

let url = root.appendingPathComponent("fixture.wav")
let frames: AVAudioFrameCount = 4800
let format = AVAudioFormat(
    commonFormat: .pcmFormatFloat32,
    sampleRate: 48000,
    channels: 2,
    interleaved: false
)!
let source = AVAudioPCMBuffer(
    pcmFormat: format, frameCapacity: frames
)!
source.frameLength = frames
let input = source.floatChannelData!
for i in 0..<Int(frames) {
    input[0][i] = 0.25
    input[1][i] = -0.5
}

func writeFixture() throws {
    let settings: [String: Any] = [
        AVFormatIDKey: kAudioFormatLinearPCM,
        AVSampleRateKey: 48000.0,
        AVNumberOfChannelsKey: 2,
        AVLinearPCMBitDepthKey: 32,
        AVLinearPCMIsFloatKey: true,
        AVLinearPCMIsBigEndianKey: false,
        AVLinearPCMIsNonInterleaved: false
    ]
    let writer = try AVAudioFile(
        forWriting: url,
        settings: settings,
        commonFormat: .pcmFormatFloat32,
        interleaved: false
    )
    try writer.write(from: source)
}

try writeFixture()

let reader = try AVAudioFile(
    forReading: url,
    commonFormat: .pcmFormatFloat32,
    interleaved: false
)
precondition(reader.fileFormat.sampleRate == 48000)
precondition(reader.fileFormat.channelCount == 2)
precondition(reader.length == AVAudioFramePosition(frames))

let output = AVAudioPCMBuffer(
    pcmFormat: reader.processingFormat,
    frameCapacity: frames + 1
)!
try reader.read(into: output)
precondition(output.frameLength == frames)

let channels = output.floatChannelData!
for channel in 0..<2 {
    let expected: Float = channel == 0 ? 0.25 : -0.5
    for i in 0..<Int(output.frameLength) {
        let value = channels[channel][i]
        precondition(value.isFinite)
        precondition(abs(value - expected) <= 0.000001)
    }
}
print("PASS: 4800 frames, 2 channels, samples verified")

쓰기 객체를 함수 안에 둔 이유는 파일을 다시 읽기 전에 객체의 수명을 끝내 파일 마무리 작업이 완료되도록 하기 위해서입니다. 아직 닫히지 않은 쓰기 객체와 읽기 객체가 같은 출력 파일에 동시에 접근하게 해서는 안 됩니다. 이 상태에서 짧게 읽힌 파일을 내보내기 코드가 잘라 낸 결과로 오인할 수 있습니다.

여기서는 디스크에 저장되는 형식과 메모리에서 처리하는 형식을 구분합니다. WAV 파일은 인터리브 방식으로 저장하고, 처리 버퍼는 비인터리브 부동소수점 데이터를 사용합니다. 채널 포인터로 데이터에 접근할 때는 반드시 processingFormat을 기준으로 삼아야 합니다. 확장자만 보고 메모리 배치를 가정해서는 안 됩니다.

실제 내보내기 코드에 검사 연결하기

최소 스크립트가 통과하면 읽기와 검증 부분은 그대로 두고, writeFixture()를 프로젝트의 실제 내보내기 인터페이스로 교체합니다. 인터페이스가 비동기로 실행된다면 테스트는 성공 콜백을 기다려야 합니다. 이때 성공은 작업이 대기열에 들어갔다는 뜻이 아니라, 파일 쓰기가 완료되었다는 뜻이어야 합니다.

한 번에 쓰기보다 청크 경계 검증이 더 중요합니다

최소한 1, 1023, 1024, 1025, 4800프레임 길이를 각각 테스트합니다. 이어서 같은 신호를 길이가 서로 다른 여러 버퍼로 나누고, 출력 프레임 수가 각 청크의 유효 프레임 수를 모두 더한 값과 같은지 검증합니다. 마지막 청크는 대개 버퍼 용량을 채우지 못하므로, 쓸 때는 frameCapacity가 아니라 frameLength를 사용해야 합니다.

처음과 끝에 임펄스 샘플을 추가하는 것도 유용합니다. 첫 프레임과 마지막 프레임에 각각 서로 다른 진폭의 샘플을 넣고 나머지는 0으로 채웁니다. 이렇게 하면 앞부분에 삽입된 무음, 마지막 프레임 누락, 중복 이어 붙이기를 찾아낼 수 있습니다. 고정 레벨 신호는 채널 매핑을, 임펄스 신호는 위치를 검사하는 데 적합하므로 두 테스트 픽스처를 서로 대체해서는 안 됩니다.

테스트 타깃으로 플랫폼별 호출 경로 검증하기

이 스크립트는 macOS에서 실행되므로, 파일 픽스처와 비교 로직이 올바르게 동작한다는 것만 확인해 줍니다. 앱의 실제 호출 경로까지 검증하려면 공유 내보내기 코드를 iOS 테스트 타깃에 포함하고, 시뮬레이터에서 같은 검사를 실행해야 합니다. 오디오 세션이나 마이크 입력과 관련된 부분은 별도로 실제 기기에서 테스트합니다.

정수 PCM에는 별도의 오차 기준 적용하기

부동소수점 샘플을 정수 PCM으로 저장하면 양자화가 발생하므로, 값이 완전히 같아야 한다는 검증 조건을 그대로 적용할 수 없습니다. 먼저 정규화 범위와 반올림 방식을 정한 다음, 최하위 비트를 기준으로 허용 오차를 설정합니다. 최대 진폭에 가까운 샘플, 음수 샘플, 값이 0인 샘플도 추가해 클리핑이 정해진 규칙대로 처리되는지 확인해야 합니다.

리샘플링에는 별도의 요구 조건이 필요합니다. 필터에서 지연이 발생할 수 있고, 출력 프레임 수는 정수 반올림과 필터에 남은 데이터를 끝까지 출력하는 처리에도 영향을 받습니다. 따라서 이 예제처럼 입력과 출력의 프레임 수가 같아야 한다는 조건을 그대로 적용할 수 없습니다. 여러 변수가 뒤섞이지 않도록, 먼저 변환 없이 그대로 내보내는 경로를 안정적으로 검증한 뒤 형식 변환을 테스트합니다.

실패하면 계층별로 증거 수집하기

프레임 수가 맞지 않으면 마지막 청크의 유효 길이와 비동기 작업의 완료 시점부터 확인합니다. 프레임 수는 맞지만 좌우가 뒤바뀌었다면 채널 매핑을 확인합니다. 모든 수치가 비정상이라면 인터리브 배치와 부동소수점·정수 형식 간 변환을 살펴봅니다. 실패할 때마다 허용 오차를 늘려서는 안 됩니다. 형식 오류를 가린 채 테스트만 통과시킬 수 있기 때문입니다.

실패할 때마다 입력 청크의 길이 순서, 파일 형식, 처리 형식, 실제 프레임 수, 최대 오차, 첫 번째 오류 샘플의 위치를 기록합니다. 테스트 산출물에는 합성 오디오만 보관해 실제 녹음이 파이프라인에 유입되지 않도록 합니다. 최종 검증 기준은 “파일이 재생된다”는 말이 아니라, 이렇게 비교 가능한 수치로 명시되어야 합니다.

자주 묻는 질문

오디오 출력 장치가 없는 클라우드 Mac에서도 WAV를 검증할 수 있나요?

가능합니다. AVAudioFile의 파일 읽기와 쓰기에는 스피커가 필요하지 않습니다. 프레임 수와 샘플 값은 오프라인으로 검사하고, 재생 경로와 청감은 별도 기기 테스트로 확인합니다.

모든 WAV의 샘플 값이 완전히 같아야 하나요?

아닙니다. 정수 PCM에는 양자화 오차가 생기고, 리샘플링이나 게인 처리도 값을 바꿉니다. 출력 형식과 처리 경로별로 허용 오차와 프레임 수 규칙을 정해야 합니다.

다음 빌드는 클라우드 워크스테이션에서

Mac mini 한 대를 단독으로 사용하며, 가상 머신이 아닙니다. 작업에 맞는 대여 기간을 선택하세요. 실제 사용 가능 여부는 콘솔에 실시간으로 표시됩니다.

클라우드 Mac 대여하기

구성을 먼저 확인하고 작업을 시작하세요

Desk M4의 요금제와 추가 옵션을 확인하거나, 시작 가이드를 보고 첫 연결을 준비하세요.