Audio #1
@@ -12,6 +12,7 @@ const (
|
|||||||
FormatVideo = "urn:x-nmos:format:video"
|
FormatVideo = "urn:x-nmos:format:video"
|
||||||
FormatAudio = "urn:x-nmos:format:audio"
|
FormatAudio = "urn:x-nmos:format:audio"
|
||||||
MediaTypeV210 = "video/v210"
|
MediaTypeV210 = "video/v210"
|
||||||
|
MediaTypeFloat32 = "audio/float32"
|
||||||
|
|
||||||
InterlaceProgressive = "progressive"
|
InterlaceProgressive = "progressive"
|
||||||
ColorSpaceBT709 = "BT709"
|
ColorSpaceBT709 = "BT709"
|
||||||
@@ -32,6 +33,22 @@ type Rational struct {
|
|||||||
Denominator uint `json:"denominator"`
|
Denominator uint `json:"denominator"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Rational) UnmarshalJSON(data []byte) error {
|
||||||
|
var value struct {
|
||||||
|
Numerator uint `json:"numerator"`
|
||||||
|
Denominator *uint `json:"denominator"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.Numerator = value.Numerator
|
||||||
|
r.Denominator = 1
|
||||||
|
if value.Denominator != nil {
|
||||||
|
r.Denominator = *value.Denominator
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type Video struct {
|
type Video struct {
|
||||||
Common
|
Common
|
||||||
|
|
||||||
@@ -141,3 +158,66 @@ func (v Video) Validate() error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewFloat32Audio(
|
||||||
|
id string,
|
||||||
|
channels uint,
|
||||||
|
rate Rational,
|
||||||
|
) (Audio, error) {
|
||||||
|
definition := Audio{
|
||||||
|
Common: Common{
|
||||||
|
Description: "go-mxl-pattern-gen generated audio",
|
||||||
|
ID: id,
|
||||||
|
Tags: map[string][]string{
|
||||||
|
"urn:x-nmos:tag:grouphint/v1.0": {
|
||||||
|
"go-mxl-pattern-gen:Audio",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Format: FormatAudio,
|
||||||
|
Label: "go-mxl-pattern-gen generated audio",
|
||||||
|
Parents: []string{},
|
||||||
|
MediaType: MediaTypeFloat32,
|
||||||
|
},
|
||||||
|
SampleRate: rate,
|
||||||
|
ChannelCount: channels,
|
||||||
|
BitDepth: 32,
|
||||||
|
}
|
||||||
|
if err := definition.Validate(); err != nil {
|
||||||
|
return Audio{}, err
|
||||||
|
}
|
||||||
|
return definition, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Audio) Validate() error {
|
||||||
|
if err := uuid.Validate(a.ID); err != nil {
|
||||||
|
return fmt.Errorf("invalid id %q: %w", a.ID, err)
|
||||||
|
}
|
||||||
|
if a.Format != FormatAudio {
|
||||||
|
return fmt.Errorf("format must be %q, got %q", FormatAudio, a.Format)
|
||||||
|
}
|
||||||
|
if a.MediaType != MediaTypeFloat32 {
|
||||||
|
return fmt.Errorf("media_type must be %q, got %q", MediaTypeFloat32, a.MediaType)
|
||||||
|
}
|
||||||
|
if a.ChannelCount == 0 {
|
||||||
|
return fmt.Errorf("channel_count must be greater than zero")
|
||||||
|
}
|
||||||
|
if a.SampleRate.Numerator == 0 || a.SampleRate.Denominator == 0 {
|
||||||
|
return fmt.Errorf("sample_rate numerator and denominator must be greater than zero, got %d/%d",
|
||||||
|
a.SampleRate.Numerator, a.SampleRate.Denominator)
|
||||||
|
}
|
||||||
|
if a.BitDepth != 32 {
|
||||||
|
return fmt.Errorf("bit_depth must be 32, got %d", a.BitDepth)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseFloat32Audio(data []byte) (Audio, error) {
|
||||||
|
var definition Audio
|
||||||
|
if err := json.Unmarshal(data, &definition); err != nil {
|
||||||
|
return Audio{}, fmt.Errorf("decode audio flow definition: %w", err)
|
||||||
|
}
|
||||||
|
if err := definition.Validate(); err != nil {
|
||||||
|
return Audio{}, fmt.Errorf("invalid audio flow definition: %w", err)
|
||||||
|
}
|
||||||
|
return definition, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const testVideoID = "5fbec3b1-1b0f-417d-9059-8b94a47197ed"
|
const testVideoID = "5fbec3b1-1b0f-417d-9059-8b94a47197ed"
|
||||||
|
const testAudioID = "b3bb5be7-9fe9-4324-a5bb-4c70e1084449"
|
||||||
|
|
||||||
func TestNewV210Video(t *testing.T) {
|
func TestNewV210Video(t *testing.T) {
|
||||||
definition, err := NewV210Video(testVideoID, 1920, 1080, Rational{Numerator: 30000, Denominator: 1001})
|
definition, err := NewV210Video(testVideoID, 1920, 1080, Rational{Numerator: 30000, Denominator: 1001})
|
||||||
@@ -65,3 +66,113 @@ func TestParseV210VideoRejectsAudio(t *testing.T) {
|
|||||||
t.Fatalf("error = %v, want video format error", err)
|
t.Fatalf("error = %v, want video format error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewFloat32Audio(t *testing.T) {
|
||||||
|
definition, err := NewFloat32Audio(testAudioID, 2, Rational{Numerator: 48000, Denominator: 1})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewFloat32Audio: %v", err)
|
||||||
|
}
|
||||||
|
if definition.ID != testAudioID {
|
||||||
|
t.Fatalf("ID = %q, want %q", definition.ID, testAudioID)
|
||||||
|
}
|
||||||
|
if definition.Format != FormatAudio || definition.MediaType != MediaTypeFloat32 {
|
||||||
|
t.Fatalf("format/media type = %q/%q", definition.Format, definition.MediaType)
|
||||||
|
}
|
||||||
|
if definition.ChannelCount != 2 || definition.BitDepth != 32 {
|
||||||
|
t.Fatalf("channels/bit depth = %d/%d, want 2/32", definition.ChannelCount, definition.BitDepth)
|
||||||
|
}
|
||||||
|
if definition.SampleRate != (Rational{Numerator: 48000, Denominator: 1}) {
|
||||||
|
t.Fatalf("sample rate = %+v, want 48000/1", definition.SampleRate)
|
||||||
|
}
|
||||||
|
if definition.Parents == nil {
|
||||||
|
t.Fatal("Parents is nil; want an empty JSON array")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewFloat32AudioRejectsZeroChannels(t *testing.T) {
|
||||||
|
_, err := NewFloat32Audio(testAudioID, 0, Rational{Numerator: 48000, Denominator: 1})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "channel_count") {
|
||||||
|
t.Fatalf("error = %v, want channel_count error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewFloat32AudioRejectsInvalidRate(t *testing.T) {
|
||||||
|
_, err := NewFloat32Audio(testAudioID, 2, Rational{Numerator: 48000, Denominator: 0})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "sample_rate") {
|
||||||
|
t.Fatalf("error = %v, want sample_rate error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseFloat32Audio(t *testing.T) {
|
||||||
|
want, err := NewFloat32Audio(testAudioID, 2, Rational{Numerator: 48000, Denominator: 1})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewFloat32Audio: %v", err)
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(want)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("json.Marshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := ParseFloat32Audio(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseFloat32Audio: %v", err)
|
||||||
|
}
|
||||||
|
if got.ID != want.ID || got.ChannelCount != want.ChannelCount || got.SampleRate != want.SampleRate {
|
||||||
|
t.Fatalf("parsed definition = %+v, want %+v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseFloat32AudioDefaultsDenominator(t *testing.T) {
|
||||||
|
data := []byte(`{
|
||||||
|
"description":"test audio",
|
||||||
|
"id":"b3bb5be7-9fe9-4324-a5bb-4c70e1084449",
|
||||||
|
"tags":{},
|
||||||
|
"format":"urn:x-nmos:format:audio",
|
||||||
|
"label":"test audio",
|
||||||
|
"parents":[],
|
||||||
|
"media_type":"audio/float32",
|
||||||
|
"sample_rate":{"numerator":48000},
|
||||||
|
"channel_count":2,
|
||||||
|
"bit_depth":32
|
||||||
|
}`)
|
||||||
|
|
||||||
|
definition, err := ParseFloat32Audio(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseFloat32Audio: %v", err)
|
||||||
|
}
|
||||||
|
if definition.SampleRate.Denominator != 1 {
|
||||||
|
t.Fatalf("denominator = %d, want implicit 1", definition.SampleRate.Denominator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseFloat32AudioRejectsExplicitZeroDenominator(t *testing.T) {
|
||||||
|
data := []byte(`{
|
||||||
|
"id":"b3bb5be7-9fe9-4324-a5bb-4c70e1084449",
|
||||||
|
"format":"urn:x-nmos:format:audio",
|
||||||
|
"media_type":"audio/float32",
|
||||||
|
"sample_rate":{"numerator":48000,"denominator":0},
|
||||||
|
"channel_count":2,
|
||||||
|
"bit_depth":32
|
||||||
|
}`)
|
||||||
|
|
||||||
|
_, err := ParseFloat32Audio(data)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "sample_rate") {
|
||||||
|
t.Fatalf("error = %v, want sample_rate error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseFloat32AudioRejectsVideo(t *testing.T) {
|
||||||
|
definition, err := NewV210Video(testVideoID, 1920, 1080, Rational{Numerator: 25, Denominator: 1})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewV210Video: %v", err)
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(definition)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("json.Marshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = ParseFloat32Audio(data)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "format must be") {
|
||||||
|
t.Fatalf("error = %v, want audio format error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user