2022-12-05-TIL
2022-12-05-TIL
Today I Learned
오디오 파일의 재생 시간과 메타데이터를 서버에서 읽어 내는 방법을 찾으면서 WAV, MP3 포맷 구조와 Java, JavaScript에서 쓸 수 있는 도구를 정리했다. 마지막에는 JPA에서 컬럼 기본값을 주는 방법을 따로 정리했다.
WAV File Format Specification
WAV는 Microsoft의 RIFF(Resource Interchange File Format) 컨테이너 위에 정의된 오디오 포맷이다. 파일은 RIFF 헤더로 시작하고 형식 이름 WAVE가 뒤따르며, 그 안에 오디오 형식을 기술하는 fmt 청크와 실제 샘플이 담긴 data 청크가 들어 있다. 보통 비압축 PCM을 담기 때문에 MP3보다 파일이 크다.
fmt 청크에는 오디오 포맷 코드(PCM은 1), 채널 수, 샘플레이트, 초당 바이트 수(ByteRate), 샘플 하나의 바이트 수(BlockAlign), 샘플당 비트 수가 들어 있다. 그래서 PCM WAV의 재생 시간은 data 청크 크기를 ByteRate로 나누면 바로 구할 수 있다. 다만 LIST 같은 부가 청크가 끼어 있는 파일도 있으므로 헤더를 고정 44바이트로 가정하지 말고 청크 ID와 크기를 따라가며 읽는 편이 안전하다.
- https://www.loc.gov/preservation/digital/formats/fdd/fdd000001.shtml
- https://www.bitsforbyte.com/2021/04/09/wave-file-format-specification/
- https://ccrma.stanford.edu/courses/422-winter-2014/projects/WaveFormat/
- http://soundfile.sapp.org/doc/WaveFormat/
- https://sites.google.com/site/musicgapi/technical-documents/wav-file-format
- https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
- https://docs.fileformat.com/audio/wav/
MP3 File Format Specification
MP3(MPEG-1/2 Audio Layer III)는 손실 압축 포맷이다. 파일 앞이나 뒤에 ID3 태그로 제목, 아티스트 같은 메타데이터가 붙을 수 있고, 본문은 독립적인 프레임의 연속이다. 각 프레임은 11개의 1비트로 된 동기 패턴으로 시작하는 헤더를 가지며, 헤더에 MPEG 버전, 레이어, 비트레이트 인덱스, 샘플레이트 인덱스가 들어 있다.
재생 시간 계산이 WAV보다 까다로운 이유가 여기에 있다. 고정 비트레이트(CBR) 파일은 파일 크기를 비트레이트로 나눠 근사할 수 있지만, 가변 비트레이트(VBR) 파일은 프레임마다 비트레이트가 달라 이 방식이 맞지 않는다. 정확한 값을 얻으려면 모든 프레임을 훑으며 프레임별 재생 시간을 더하거나, VBR 인코더가 첫 프레임에 넣어 두는 Xing 헤더(총 프레임 수 등 파일 전체 정보)를 읽어야 한다. mp3inside 문서는 이 문자열이 없으면 CBR 파일로 간주한다고 설명한다.
- https://www.loc.gov/preservation/digital/formats/fdd/fdd000105.shtml
- http://www.multiweb.cz/twoinches/mp3inside.htm
- https://docs.fileformat.com/audio/mp3/
- https://en.wikipedia.org/wiki/MP3
- http://mpgedit.org/mpgedit/mpeg_format/MP3Format.html
- https://www.loc.gov/preservation/digital/formats/fdd/fdd000012.shtml
Using FFmpeg to Get an MP3’s Duration and 4 Ways to Get the File Size
라이브러리로 직접 파싱하지 않고 FFmpeg에 포함된 ffprobe로 재생 시간을 얻는 방법도 있다. ffprobe는 멀티미디어 스트림 정보를 사람이 읽거나 기계가 파싱할 수 있는 형식으로 출력하는 도구라서, JSON 출력 옵션을 주고 결과를 읽으면 포맷과 상관없이 같은 방식으로 duration을 얻을 수 있다. 대신 서버에 바이너리를 설치하고 프로세스를 띄우는 비용이 든다.
- https://nickjanetakis.com/blog/using-ffmpeg-to-get-an-mp3s-duration-and-4-ways-to-get-the-file-size
JavaScript get Duration
브라우저에서는 Audio 요소나 Web Audio API로 파일을 로드한 뒤 loadedmetadata 이벤트에서 duration을 읽는 방식이 흔하다. 브라우저가 디코딩을 대신해 주므로 구현은 간단하지만, 서버 측 검증 용도로는 쓸 수 없다. 마지막 글은 VBR 문제 때문에 프레임을 하나씩 파싱해 합산하는 과정을 직접 구현한다.
- https://stackoverflow.com/questions/71118040/getting-the-duration-of-an-mp3-file-in-a-variable
- https://ourcodeworld.com/articles/read/1036/how-to-retrieve-the-duration-of-a-mp3-wav-audio-file-in-the-browser-with-javascript
- https://shadowfacts.net/2021/mp3-duration/
Java Sound API
What audio formats does Java Sound support? Java Sound supports the following audio file formats: AIFF, AU and WAV. It also supports the following MIDI based song file formats: SMF type 0 (Standard MIDI File, aka .mid files), SMF type 1 and RMF.
MP3 등 여러가지 압축 포맷은 지원하지 않는다.
javax.sound.sampled의 AudioFileFormat은 선택적인 프로퍼티로 "duration"(마이크로초 단위 Long)을 정의하지만, 파일 리더 구현이 이 값을 반드시 채워야 하는 것은 아니다. WAV라면 AudioInputStream의 프레임 수를 프레임레이트로 나눠 직접 계산하는 편이 확실하다.
1
2
3
4
try (AudioInputStream in = AudioSystem.getAudioInputStream(file)) {
AudioFormat format = in.getFormat();
double seconds = in.getFrameLength() / format.getFrameRate();
}
MP3는 기본 API로 열리지 않으므로 별도 SPI 구현이나 메타데이터 라이브러리, 또는 앞에서 본 ffprobe를 써야 한다. 아래 링크의 Stack Overflow 답변들도 대부분 이 세 갈래 중 하나를 권한다.
- https://stackoverflow.com/questions/10645594/what-audio-format-should-i-use-for-java
- https://docs.oracle.com/javase/8/docs/technotes/guides/sound/index.html
- https://developers.google.com/resources/api-libraries/documentation/youtube/v3/java/latest/com/google/api/services/youtube/model/VideoFileDetails.html
- https://docs.oracle.com/javase/7/docs/api/javax/sound/sampled/Clip.html
- https://docs.oracle.com/javase/7/docs/api/javax/sound/sampled/AudioFileFormat.html
- https://stackoverflow.com/questions/3046669/how-do-i-get-a-mp3-files-total-time-in-java
- https://stackoverflow.com/questions/72009794/how-can-i-extract-metadata-from-a-mp4-and-a-avi-with-native-library-in-java
- https://stackoverflow.com/questions/1645803/how-to-read-mp3-file-tags#answer-21746415
- https://community.oracle.com/tech/developers/discussion/2416733/get-the-duration-of-all-mp3-files-inside-a-directory
- https://docs.oracle.com/javase/tutorial/sound/
- https://www.tabnine.com/code/java/methods/javax.sound.sampled.AudioFileFormat/properties
- https://docs.oracle.com/en/java/javase/19/docs/api/java.desktop/javax/sound/sampled/AudioFileFormat.html
- https://www.youtube.com/watch?v=nUKya2DvYSo
- https://gist.github.com/EslamElassal/4a0a165db71680eb0f80
- https://stackoverflow.com/questions/47957848/java-getting-the-metadata-tags-from-a-wave-audio-file
- https://itecnote.com/tecnote/java-play-load-mp3-using-java-apis-and-jmf-error-unsupported-format/
- https://stackoverflow.com/questions/2709508/how-to-learn-wav-duration-in-java-media-frame-work
- https://stackoverflow.com/questions/30183642/how-to-get-duration-of-audio-file
Extract Metadata of MP3 Files using REST API in Java
라이브러리를 들이지 않고 외부 SaaS의 메타데이터 추출 API를 호출하는 방법도 있다. 파일을 외부로 보내야 하므로 비용과 데이터 반출 정책을 함께 따져야 한다.
- https://blog.groupdocs.cloud/2021/07/09/extract-metadata-of-mp3-files-using-rest-api-in-java/
Java Managing Metadata
java.nio.file의 파일 속성 API(BasicFileAttributes 등)가 다루는 것은 크기, 생성·수정 시각 같은 파일 시스템 메타데이터다. 오디오의 재생 시간이나 ID3 태그 같은 컨텐츠 메타데이터와는 다르다는 점을 구분해야 한다.
- https://docs.oracle.com/javase/tutorial/essential/io/fileAttr.html
- https://stackoverflow.com/questions/24797585/extracting-metadata-in-java
How to Extract Metadata from Music Files in JavaScript
- https://www.youtube.com/watch?v=6TQab8AVdVQ
JPA Column Default Value
엔티티에서 값을 채우지 않은 필드는 null로 INSERT 문에 포함되기 때문에, DDL에 DEFAULT를 걸어 두어도 적용되지 않는 경우가 생긴다. 기본값을 주는 방법은 대략 네 가지다.
- 필드 초기값:
private Status status = Status.READY;처럼 자바 쪽에서 값을 정한다. 가장 단순하고 DB와 무관하다. @Column(columnDefinition = "..."), Hibernate의@ColumnDefault: 스키마 생성 시 DDL에 DEFAULT를 넣는다. INSERT에null이 들어가면 여전히 기본값이 적용되지 않는다.- Hibernate
@DynamicInsert:null인 컬럼을 INSERT 문에서 빼서 DB 기본값이 적용되게 한다. 엔티티마다 SQL을 동적으로 만든다는 비용이 있다. @PrePersist콜백에서 비어 있는 값을 채운다.- https://thorben-janssen.com/dynamic-inserts-and-updates-with-spring-data-jpa/
- https://eocoding.tistory.com/71
- https://www.baeldung.com/jpa-default-column-values
- https://dotoridev.tistory.com/6
- https://goodteacher.tistory.com/369
- https://gaemi606.tistory.com/entry/JPA-DynamicInsert-DynamicUpdate-%EC%97%94%ED%8B%B0%ED%8B%B0-%EB%A6%AC%EC%8A%A4%EB%84%88
- https://mangchhe.github.io/jpa/2021/09/06/EntityDynamicQuery/
댓글
아직 댓글이 없습니다