Updated Kirika, new option system for codecs
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
DataHoarder 2022-05-15 16:42:28 +02:00
parent 24e2750cbe
commit 8eb0bf95c6
Signed by: DataHoarder
SSH key fingerprint: SHA256:OLTRf6Fl87G52SiR7sWLGNzlJt4WOX+tfI2yxo0z7xk
8 changed files with 338 additions and 84 deletions

View file

@ -14,6 +14,7 @@ Radio streamer ([kawa](https://github.com/Luminarys/kawa) drop-in compatible).
* Implements `queue.nr` and `/random` (to be deprecated/changed)
* Supports max queue length via `queue.length` config.
* Supports extra encoder bitrate control settings (CBR, VBR, auto, etc.)
* Can set custom sample rate / channel count / bitdepth / compression level per stream mount.
* Can read and apply ReplayGain tags, or normalize audio loudness.
* Can have audio sources over HTTP(s) URLs on `path` property, and supports seeking.
* Precise metadata and timing information packet stream, trigger via `x-audio-packet-stream: 1` HTTP header.
@ -93,4 +94,86 @@ Same as kawa's, but `queue_id` is added to response directly.
"reason": null,
"queue_id": 5
}
```
## Mount API
### `NEW` GET /mounts
A simple listing of the working mounts + settings are made available.
#### Response
```json
[
{
"mount": "/stream128.mp3",
"mime": "audio/mpeg;codecs=mp3",
"sampleRate": 44100,
"channels": 2,
"listeners": 0,
"options": {
"bitrate": 128
}
},
{
"mount": "/stream192.mp3",
"mime": "audio/mpeg;codecs=mp3",
"sampleRate": 44100,
"channels": 2,
"listeners": 0,
"options": {
"bitrate": 192
}
},
{
"mount": "/stream128.aac",
"mime": "audio/aac",
"sampleRate": 44100,
"channels": 2,
"listeners": 0,
"options": {
"bitrate": 128
}
},
{
"mount": "/stream128.opus",
"mime": "audio/ogg;codecs=opus",
"sampleRate": 48000,
"channels": 2,
"listeners": 0,
"options": {
"bitrate": 128
}
},
{
"mount": "/stream192.opus",
"mime": "audio/ogg;codecs=opus",
"sampleRate": 48000,
"channels": 2,
"listeners": 0,
"options": {
"bitrate": 192
}
},
{
"mount": "/stream256.opus",
"mime": "audio/ogg;codecs=opus",
"sampleRate": 48000,
"channels": 2,
"listeners": 0,
"options": {
"bitrate": 256
}
},
{
"mount": "/stream.flac",
"mime": "audio/flac",
"sampleRate": 44100,
"channels": 2,
"listeners": 0,
"options": {
"bitdepth": 16,
"block_size": 0,
"compression_level": 5
}
}
]
```

6
api.go
View file

@ -4,7 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
"git.gammaspectra.live/S.O.N.G/Kirika/audio/queue"
"io/ioutil"
"log"
"net/http"
@ -152,7 +152,7 @@ func (a *API) listen() {
type queueResultResponse struct {
resultResponse
QueueId audio.QueueIdentifier `json:"queue_id"`
QueueId queue.QueueIdentifier `json:"queue_id"`
}
a.wg.Add(1)
@ -290,7 +290,7 @@ func (a *API) listen() {
if i, err := strconv.ParseInt(pathSegments[2], 10, 0); err == nil {
result := resultResponse{}
result.Success = a.queue.Remove(audio.QueueIdentifier(i))
result.Success = a.queue.Remove(queue.QueueIdentifier(i))
jsonData, _ := json.Marshal(result)
writer.Write(jsonData)

View file

@ -1,6 +1,10 @@
package main
import "github.com/BurntSushi/toml"
import (
"errors"
"github.com/BurntSushi/toml"
"strconv"
)
type Config struct {
Api struct {
@ -16,6 +20,7 @@ type Config struct {
BufferSeconds int `toml:"buffer_size"`
ReplayGain bool `toml:"replaygain"`
Length int `toml:"length"`
SampleRate int `toml:"samplerate"`
} `toml:"queue"`
Radio struct {
Port int `toml:"port"`
@ -25,12 +30,90 @@ type Config struct {
Logo string `toml:"logo"`
Private bool `toml:"private"`
} `toml:"radio"`
Streams []struct {
MountPath string `toml:"mount"`
Container string `toml:"container"`
Bitrate interface{} `toml:"bitrate"`
Codec string `toml:"codec"`
} `toml:"streams"`
Streams []StreamConfig `toml:"streams"`
}
type StreamConfig struct {
MountPath string
Container string
Codec string
Options map[string]interface{}
}
func (s *StreamConfig) GetIntOption(name string, defaultValue int) int {
if v, ok := s.Options[name].(int); ok {
return v
}
if v, ok := s.Options[name].(int64); ok {
return int(v)
}
if v, ok := s.Options[name].(string); ok {
if val, err := strconv.Atoi(v); err == nil {
return val
}
}
return defaultValue
}
func (s *StreamConfig) GetStringOption(name string, defaultValue string) string {
if v, ok := s.Options[name].(string); ok {
return v
}
if v, ok := s.Options[name].(int); ok {
return strconv.Itoa(v)
}
if v, ok := s.Options[name].(int64); ok {
return strconv.Itoa(int(v))
}
return defaultValue
}
func (s *StreamConfig) GetOption(name string, defaultValue interface{}) interface{} {
if v, ok := s.Options[name]; ok {
return v
}
return defaultValue
}
func (s *StreamConfig) UnmarshalTOML(value interface{}) error {
if v, ok := value.(map[string]interface{}); ok {
if mount, ok := v["mount"].(string); ok {
s.MountPath = mount
} else {
return errors.New("could not find mount path")
}
if container, ok := v["container"].(string); ok {
s.Container = container
}
if codec, ok := v["codec"].(string); ok {
s.Codec = codec
}
s.Options = make(map[string]interface{})
for k, val := range v {
if k == "mount" || k == "container" || k == "codec" {
continue
}
s.Options[k] = val
}
return nil
}
return errors.New("could not decode")
}
func GetConfig(pathName string) (*Config, error) {

View file

@ -53,6 +53,10 @@ buffer_duration=0
# }
replaygain=false
# Set the sample rate of the queue. Default is 44100
# Some codecs (example: Opus) will output at a different samplerate.
#samplerate=44100
[radio]
#
# The port to stream actual audio on. MeteorLight will listen on localhost.
@ -73,11 +77,15 @@ private=false
# following properties are available:
#
# mount: the HTTP address to serve the stream from
# container: the container format to use (ogg, flac, aac, or mp3). See Kirika's supported format list.
# codec: the audio codec to use (opus, flac, aac, do not specify for mp3 streams)
# container: the container format to use (ogg, flac, aac/adts, or mp3). See Kirika's supported format list.
# codec: the audio codec to use (opus, flac, aac, he-aacv2, or mp3)
# bitrate: the desired bitrate of the stream in Kb/s, if not specified (or 0) an appropriate
# bitrate will be automatically selected based on the container/codec
# MeteorLight extension: bitrate can be a string (for example, v0/v1/v2/v3 on MP3). codec can also be he-aacv2. No vorbis support
# MeteorLight extension: bitrate can be a string (for example, v0/v1/v2/v3 on MP3). codec can also be he-aacv2. No vorbis support.
# Additionally further options can be set on some codecs.
# - FLAC supports bitdepth (int), compression_level (int), block_size (int)
# - All but Opus support setting samplerate
# - All support setting channels to 1 or 2
[[streams]]
mount="stream128.mp3"
container="mp3"
@ -90,6 +98,8 @@ bitrate=192
[[streams]]
mount="stream128.aac"
# Also supports he-aacv2
codec="aac"
container="aac"
bitrate=128
@ -113,4 +123,6 @@ bitrate=256
[[streams]]
mount="stream.flac"
codec="flac"
# Also supports ogg
container="flac"

11
go.mod
View file

@ -3,21 +3,22 @@ module git.gammaspectra.live/S.O.N.G/MeteorLight
go 1.18
require (
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220421115636-4612766b64e8
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220515143733-468ec9b491d1
github.com/BurntSushi/toml v1.1.0
github.com/dhowden/tag v0.0.0-20201120070457-d52dcb253c63
github.com/enriquebris/goconcurrentqueue v0.6.3
)
require (
git.gammaspectra.live/S.O.N.G/go-alac v0.0.0-20220421110341-7839cd4c1da1 // indirect
git.gammaspectra.live/S.O.N.G/go-alac v0.0.0-20220421115623-d0b3bfe57e0f // indirect
git.gammaspectra.live/S.O.N.G/go-ebur128 v0.0.0-20220418202343-73a167e76255 // indirect
git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220417020459-7018d91e3eed // indirect
git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220421165127-c4b73b260d94 // indirect
git.gammaspectra.live/S.O.N.G/go-pus v0.0.0-20220227175608-6cc027f24dba // indirect
git.gammaspectra.live/S.O.N.G/go-tta v0.2.1-0.20220226150007-096de1072bd6 // indirect
git.gammaspectra.live/S.O.N.G/goflac v0.0.0-20220417181802-3057bde44c07 // indirect
github.com/dh1tw/gosamplerate v0.1.2 // indirect
github.com/edgeware/mp4ff v0.27.0 // indirect
github.com/edgeware/mp4ff v0.28.0 // indirect
github.com/gen2brain/aac-go v0.0.0-20180306134136-400c68157565 // indirect
github.com/icza/bitio v1.1.0 // indirect
github.com/jfreymuth/oggvorbis v1.0.3 // indirect
github.com/jfreymuth/vorbis v1.0.2 // indirect
@ -27,4 +28,6 @@ require (
github.com/mewkiz/pkg v0.0.0-20211102230744-16a6ce8f1b77 // indirect
github.com/sssgun/mp3 v0.0.0-20170810093403-85f2ec632081 // indirect
github.com/viert/go-lame v0.0.0-20201108052322-bb552596b11d // indirect
github.com/youpy/go-riff v0.1.0 // indirect
github.com/zaf/g711 v0.0.0-20220109202201-cf0017bf0359 // indirect
)

27
go.sum
View file

@ -1,11 +1,11 @@
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220421115636-4612766b64e8 h1:OMLUwQmbveOsXlQU0XUfA7tZMAzz98B7rac4GKmrY90=
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220421115636-4612766b64e8/go.mod h1:YAH1ahOjNcVgGGeudk5q0bNqcSesc19ItsQeiuNART8=
git.gammaspectra.live/S.O.N.G/go-alac v0.0.0-20220421110341-7839cd4c1da1 h1:D1VyacBGUBfvFD4Fq2eO6RQ5eZPUdC2YsIv9gXun9aQ=
git.gammaspectra.live/S.O.N.G/go-alac v0.0.0-20220421110341-7839cd4c1da1/go.mod h1:f1+h7KOnuM9zcEQp7ri4UaVvgX4m1NFFIXgReIyjGMA=
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220515143733-468ec9b491d1 h1:3k49C82QXtvONyQk51XPz2cGIOkmhmmMAggWM/RW8tg=
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220515143733-468ec9b491d1/go.mod h1:65QohfAwsgMkXS7PZO2cm8VgPEZlhKC+JAeZfJBYpFE=
git.gammaspectra.live/S.O.N.G/go-alac v0.0.0-20220421115623-d0b3bfe57e0f h1:CxN7zlk5FdAieyRKQSbwBGBsvQ2cDF8JVCODZpzcRkA=
git.gammaspectra.live/S.O.N.G/go-alac v0.0.0-20220421115623-d0b3bfe57e0f/go.mod h1:f1+h7KOnuM9zcEQp7ri4UaVvgX4m1NFFIXgReIyjGMA=
git.gammaspectra.live/S.O.N.G/go-ebur128 v0.0.0-20220418202343-73a167e76255 h1:BWRx2ZFyhp5+rsXhdDZtk5Gld+L44lxlN9ASqB9Oj0M=
git.gammaspectra.live/S.O.N.G/go-ebur128 v0.0.0-20220418202343-73a167e76255/go.mod h1:5H4eVW9uknpn8REFr+C3ejhvXdncgm/pbGqKGC43gFY=
git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220417020459-7018d91e3eed h1:aJPCb3LS4Wai34S5bKUGgAm+/hPt0J2tvWPOl6RnbbE=
git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220417020459-7018d91e3eed/go.mod h1:pkWt//S9hLVEQaJDPu/cHHPk8vPpo/0+zHy0me4LIP4=
git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220421165127-c4b73b260d94 h1:gD6lOyQwwuyJilwbLC8lAEWpUSZvndJsUGwxxDatCAE=
git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220421165127-c4b73b260d94/go.mod h1:pkWt//S9hLVEQaJDPu/cHHPk8vPpo/0+zHy0me4LIP4=
git.gammaspectra.live/S.O.N.G/go-pus v0.0.0-20220227175608-6cc027f24dba h1:JEaxCVgdr3XXAuDCPAx7ttLFZaaHzTEzG+oRnVUtUKU=
git.gammaspectra.live/S.O.N.G/go-pus v0.0.0-20220227175608-6cc027f24dba/go.mod h1:vkoHSHVM9p6vAUmXAik0gvaLcIfiQYrD6bQqVpOulUk=
git.gammaspectra.live/S.O.N.G/go-tta v0.2.1-0.20220226150007-096de1072bd6 h1:ITVVisbHPnUclp3PBkCbXFeBhOCBcOjPdgjJ9wRH3TI=
@ -21,15 +21,17 @@ github.com/dh1tw/gosamplerate v0.1.2 h1:oyqtZk67xB9B4l+vIZCZ3F0RYV/z66W58VOah11/
github.com/dh1tw/gosamplerate v0.1.2/go.mod h1:zooTyHpoR7hE+FLfdE3yjLHb2QA2NpMusNfuaZqEACM=
github.com/dhowden/tag v0.0.0-20201120070457-d52dcb253c63 h1:/u5RVRk3Nh7Zw1QQnPtUH5kzcc8JmSSRpHSlGU/zGTE=
github.com/dhowden/tag v0.0.0-20201120070457-d52dcb253c63/go.mod h1:SniNVYuaD1jmdEEvi+7ywb1QFR7agjeTdGKyFb0p7Rw=
github.com/edgeware/mp4ff v0.27.0 h1:Lv773H6c4pUt3Zj9z44TOICv6fjd6gKzi1sVl8GbcYE=
github.com/edgeware/mp4ff v0.27.0/go.mod h1:6VHE5CTkpDseIg775+rh8BfnTvqjMnVbz5EDU4QwSdc=
github.com/edgeware/mp4ff v0.28.0 h1:iAtPUXtENORvCPCLIdDNeh5Lb6GXS1hIyYUx2iBkygE=
github.com/edgeware/mp4ff v0.28.0/go.mod h1:GNUeA6tEFksH2CrjJF2FSGdJolba8yPGmo16qZTXsm8=
github.com/enriquebris/goconcurrentqueue v0.6.3 h1:+ma7EEEFMDmJBIS6Q4KNJruChctgwYQFqlxdveIoEE4=
github.com/enriquebris/goconcurrentqueue v0.6.3/go.mod h1:OZ+KC2BcRYzjg0vgoUs1GFqdAjkD9mz2Ots7Jbm1yS4=
github.com/gen2brain/aac-go v0.0.0-20180306134136-400c68157565 h1:5cGj3eU0uEzpHDFwqmcKc6zM1+HNvzPIR6ZY7T1/MsA=
github.com/gen2brain/aac-go v0.0.0-20180306134136-400c68157565/go.mod h1:Tymn6PFuf0SYIYLOWIZWtatGOmi/5FLUNawJt9OgxJw=
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
github.com/go-test/deep v1.0.6 h1:UHSEyLZUwX9Qoi99vVwvewiMC8mM2bf7XEM2nqvzEn8=
github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
github.com/icza/bitio v1.1.0 h1:ysX4vtldjdi3Ygai5m1cWy4oLkhWTAi+SyO6HC8L9T0=
github.com/icza/bitio v1.1.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
@ -56,6 +58,11 @@ github.com/sssgun/mp3 v0.0.0-20170810093403-85f2ec632081/go.mod h1:ExwZtltybPz8z
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/viert/go-lame v0.0.0-20201108052322-bb552596b11d h1:LptdD7GTUZeklomtW5vZ1AHwBvDBUCZ2Ftpaz7uEI7g=
github.com/viert/go-lame v0.0.0-20201108052322-bb552596b11d/go.mod h1:EqTcYM7y4JlSfeTI47pmNu3EZQuCuLQefsQyg1Imlz8=
github.com/youpy/go-riff v0.1.0 h1:vZO/37nI4tIET8tQI0Qn0Y79qQh99aEpponTPiPut7k=
github.com/youpy/go-riff v0.1.0/go.mod h1:83nxdDV4Z9RzrTut9losK7ve4hUnxUR8ASSz4BsKXwQ=
github.com/youpy/go-wav v0.3.2 h1:NLM8L/7yZ0Bntadw/0h95OyUsen+DQIVf9gay+SUsMU=
github.com/zaf/g711 v0.0.0-20220109202201-cf0017bf0359 h1:P9yeMx2iNJxJqXEwLtMjSwWcD2a0AlFmFByeosMZhLM=
github.com/zaf/g711 v0.0.0-20220109202201-cf0017bf0359/go.mod h1:ySLGJD8AQluMQuu5JDvfJrwsBra+8iX1jFsKS8KfB2I=
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

112
mount.go
View file

@ -2,6 +2,7 @@ package main
import (
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
"git.gammaspectra.live/S.O.N.G/Kirika/audio/filter"
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format"
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/aac"
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/flac"
@ -32,18 +33,20 @@ type StreamListener struct {
Close func()
}
type StreamMount struct {
Mount string
MimeType string
Packetizer packetizer.Packetizer
Options map[string]interface{}
SampleRate int
MetadataQueue *goconcurrentqueue.FIFO
listeners []*StreamListener
listenersLock sync.Mutex
keepBuffer []packetizer.Packet
Mount string
MimeType string
FormatDescription string
Packetizer packetizer.Packetizer
Options map[string]interface{}
SampleRate int
Channels int
MetadataQueue *goconcurrentqueue.FIFO
listeners []*StreamListener
listenersLock sync.Mutex
keepBuffer []packetizer.Packet
}
func NewStreamMount(source audio.Source, mount string, codec string, container string, bitrate interface{}) *StreamMount {
func NewStreamMount(source audio.Source, config *StreamConfig) *StreamMount {
var encoderFormat format.Encoder
options := make(map[string]interface{})
var mimeType string
@ -51,14 +54,21 @@ func NewStreamMount(source audio.Source, mount string, codec string, container s
reader, writer := io.Pipe()
var packetizerEntry packetizer.Packetizer
sampleRate := source.SampleRate
bitrate := config.GetOption("bitrate", nil)
switch codec {
sampleRate := config.GetIntOption("samplerate", source.SampleRate)
channels := config.GetIntOption("channels", source.Channels)
switch config.Codec {
case "vorbis":
return nil
case "opus":
encoderFormat = opus.NewFormat()
if bitrate != nil {
options["bitrate"] = bitrate
}
sampleRate = opus.FixedSampleRate
mimeType = "audio/ogg;codecs=opus"
packetizerEntry = packetizer.NewOggPacketizer(reader)
case "mp3":
@ -70,18 +80,23 @@ func NewStreamMount(source audio.Source, mount string, codec string, container s
packetizerEntry = packetizer.NewMp3Packetizer(reader)
case "flac":
encoderFormat = flac.NewFormat()
if bitrate != nil {
options["bitdepth"] = bitrate
}
options["compression_level"] = 8
options["bitdepth"] = config.GetIntOption("bitdepth", 16)
options["compression_level"] = config.GetIntOption("compression_level", 8)
options["block_size"] = config.GetIntOption("block_size", 0)
mimeType = "audio/flac"
if config.Container == "ogg" {
options["ogg"] = true
mimeType = "audio/ogg;codecs=flac"
}
packetizerEntry = packetizer.NewFLACPacketizer(reader)
case "aac":
encoderFormat = aac.NewFormat()
if bitrate != nil {
options["bitrate"] = bitrate
}
mimeType = "audio/aac"
mimeType = "audio/aac;codecs=mp4a.40.2"
packetizerEntry = packetizer.NewAdtsPacketizer(reader)
case "he-aacv2":
encoderFormat = aac.NewFormat()
@ -89,17 +104,18 @@ func NewStreamMount(source audio.Source, mount string, codec string, container s
options["bitrate"] = bitrate
}
options["mode"] = "hev2"
mimeType = "audio/aac"
mimeType = "audio/aac;codecs=mp4a.40.29"
packetizerEntry = packetizer.NewAdtsPacketizer(reader)
}
if encoderFormat == nil {
switch container {
switch config.Container {
case "ogg":
encoderFormat = opus.NewFormat()
if bitrate != nil {
options["bitrate"] = bitrate
}
sampleRate = opus.FixedSampleRate
mimeType = "audio/ogg;codecs=opus"
packetizerEntry = packetizer.NewOggPacketizer(reader)
case "mp3":
@ -111,10 +127,11 @@ func NewStreamMount(source audio.Source, mount string, codec string, container s
packetizerEntry = packetizer.NewMp3Packetizer(reader)
case "flac":
encoderFormat = flac.NewFormat()
if bitrate != nil {
options["bitdepth"] = bitrate
}
options["compression_level"] = 8
options["bitdepth"] = config.GetIntOption("bitdepth", 16)
options["compression_level"] = config.GetIntOption("compression_level", 8)
options["block_size"] = config.GetIntOption("block_size", 0)
mimeType = "audio/flac"
packetizerEntry = packetizer.NewFLACPacketizer(reader)
case "adts", "aac":
@ -122,7 +139,7 @@ func NewStreamMount(source audio.Source, mount string, codec string, container s
if bitrate != nil {
options["bitrate"] = bitrate
}
mimeType = "audio/aac"
mimeType = "audio/aac;codecs=mp4a.40.2"
packetizerEntry = packetizer.NewAdtsPacketizer(reader)
}
}
@ -131,30 +148,35 @@ func NewStreamMount(source audio.Source, mount string, codec string, container s
return nil
}
if opusFormat, ok := encoderFormat.(opus.Format); ok {
sampleRate = opus.FixedSampleRate
go func() {
defer writer.Close()
if err := opusFormat.Encode(audio.NewResampleFilter(opus.FixedSampleRate, audio.Linear, 0).Process(source), writer, options); err != nil {
log.Panic(err)
go func() {
defer writer.Close()
if channels != source.Channels {
if channels == 1 {
source = filter.MonoFilter{}.Process(source)
} else if channels == 2 {
source = filter.StereoFilter{}.Process(source)
}
}()
} else {
go func() {
defer writer.Close()
if err := encoderFormat.Encode(source, writer, options); err != nil {
log.Panic(err)
}
}()
}
}
if sampleRate != source.SampleRate {
source = filter.NewResampleFilter(sampleRate, filter.Linear, 0).Process(source)
}
if err := encoderFormat.Encode(source, writer, options); err != nil {
log.Panic(err)
}
}()
return &StreamMount{
Mount: mount,
MimeType: mimeType,
Packetizer: packetizerEntry,
SampleRate: sampleRate,
Options: options,
MetadataQueue: goconcurrentqueue.NewFIFO(),
Mount: config.MountPath,
MimeType: mimeType,
FormatDescription: encoderFormat.Description(),
Packetizer: packetizerEntry,
SampleRate: sampleRate,
Channels: channels,
Options: options,
MetadataQueue: goconcurrentqueue.NewFIFO(),
}
}

View file

@ -7,8 +7,10 @@ import (
"errors"
"fmt"
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
"git.gammaspectra.live/S.O.N.G/Kirika/audio/filter"
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/guess"
"git.gammaspectra.live/S.O.N.G/Kirika/audio/packetizer"
"git.gammaspectra.live/S.O.N.G/Kirika/audio/queue"
"git.gammaspectra.live/S.O.N.G/Kirika/audio/replaygain"
"github.com/dhowden/tag"
"io"
@ -26,7 +28,7 @@ import (
const maxBufferSize = 10
type QueueTrackEntry struct {
QueueIdentifier audio.QueueIdentifier
QueueIdentifier queue.QueueIdentifier
Path string
Metadata struct {
Title string `json:"title"`
@ -180,7 +182,7 @@ type Queue struct {
QueueEmpty chan *QueueTrackEntry
Duration time.Duration
durationError int64
audioQueue *audio.Queue
audioQueue *queue.Queue
mounts []*StreamMount
queue []*QueueTrackEntry
mutex sync.RWMutex
@ -189,17 +191,20 @@ type Queue struct {
}
func NewQueue(config *Config) *Queue {
if config.Queue.SampleRate <= 0 {
config.Queue.SampleRate = 44100
}
q := &Queue{
NowPlaying: make(chan *QueueTrackEntry, 1),
QueueEmpty: make(chan *QueueTrackEntry),
config: config,
audioQueue: audio.NewQueue(44100, 2),
audioQueue: queue.NewQueue(config.Queue.SampleRate, 2),
}
blocksPerSecond := 20
sources := SplitAudioSource(audio.NewFilterChain(q.audioQueue.GetSource(), audio.NewBufferFilter(16), audio.NewRealTimeFilter(blocksPerSecond), audio.NewBufferFilter(maxBufferSize*blocksPerSecond)), len(config.Streams))
sources := SplitAudioSource(filter.NewFilterChain(q.audioQueue.GetSource(), filter.NewBufferFilter(16), filter.NewRealTimeFilter(blocksPerSecond), filter.NewBufferFilter(maxBufferSize*blocksPerSecond)), len(config.Streams))
for i, s := range q.config.Streams {
mount := NewStreamMount(sources[i], s.MountPath, s.Codec, s.Container, s.Bitrate)
mount := NewStreamMount(sources[i], &s)
if mount == nil {
log.Panicf("could not initialize %s\n", s.MountPath)
}
@ -222,7 +227,7 @@ func (q *Queue) AddTrack(entry *QueueTrackEntry, tail bool) error {
return err
}
startCallback := func(queue *audio.Queue, queueEntry *audio.QueueEntry) {
startCallback := func(queue *queue.Queue, queueEntry *queue.QueueEntry) {
if e := q.Get(queueEntry.Identifier); e != nil { //is this needed?
log.Printf("now playing %s\n", e.Path)
q.NowPlaying <- e
@ -238,11 +243,11 @@ func (q *Queue) AddTrack(entry *QueueTrackEntry, tail bool) error {
}
}
endCallback := func(queue *audio.Queue, entry *audio.QueueEntry) {
endCallback := func(queue *queue.Queue, entry *queue.QueueEntry) {
}
removeCallback := func(queue *audio.Queue, entry *audio.QueueEntry) {
removeCallback := func(queue *queue.Queue, entry *queue.QueueEntry) {
atomic.AddInt64((*int64)(&q.Duration), int64((time.Second*time.Duration(entry.ReadSamples))/time.Duration(entry.Source.SampleRate)))
q.Remove(entry.Identifier)
@ -307,7 +312,7 @@ func (q *Queue) GetQueue() (result []*QueueTrackEntry) {
return
}
func (q *Queue) Get(identifier audio.QueueIdentifier) *QueueTrackEntry {
func (q *Queue) Get(identifier queue.QueueIdentifier) *QueueTrackEntry {
q.mutex.RLock()
defer q.mutex.RUnlock()
for _, e := range q.queue {
@ -359,7 +364,7 @@ func (q *Queue) GetTail() *QueueTrackEntry {
return nil
}
func (q *Queue) Remove(identifier audio.QueueIdentifier) bool {
func (q *Queue) Remove(identifier queue.QueueIdentifier) bool {
q.mutex.Lock()
for i, e := range q.queue {
if e.QueueIdentifier == identifier {
@ -434,6 +439,45 @@ func (p *packetStreamFrame) Encode() []byte {
}
func (q *Queue) HandleRadioRequest(writer http.ResponseWriter, request *http.Request) {
if strings.HasSuffix(request.URL.Path, "mounts") {
writer.Header().Set("Server", "MeteorLight/radio")
writer.Header().Set("Accept-Ranges", "none")
writer.Header().Set("Connection", "close")
writer.Header().Set("Access-Control-Allow-Origin", "*")
writer.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Icy-Metadata")
writer.Header().Set("Access-Control-Expose-Headers", "Accept-Ranges, Server, Content-Type")
type mountData struct {
Path string `json:"mount"`
MimeType string `json:"mime"`
FormatDescription string `json:"formatDescription"`
SampleRate int `json:"sampleRate"`
Channels int `json:"channels"`
Listeners int `json:"listeners"`
Options map[string]interface{} `json:"options"`
}
var mounts []mountData
for _, mount := range q.mounts {
mounts = append(mounts, mountData{
Path: strings.TrimSuffix(request.URL.Path, "mounts") + mount.Mount,
MimeType: mount.MimeType,
SampleRate: mount.SampleRate,
FormatDescription: mount.FormatDescription,
Channels: mount.Channels,
Listeners: len(mount.listeners),
Options: mount.Options,
})
}
jsonBytes, _ := json.MarshalIndent(mounts, "", " ")
writer.WriteHeader(http.StatusOK)
writer.Write(jsonBytes)
return
}
for _, mount := range q.mounts {
if strings.HasSuffix(request.URL.Path, mount.Mount) {
writer.Header().Set("Server", "MeteorLight/radio")
@ -510,7 +554,7 @@ func (q *Queue) HandleRadioRequest(writer http.ResponseWriter, request *http.Req
}
writer.Header().Set("icy-br", fmt.Sprintf("%d", bitrate))
writer.Header().Set("icy-sr", fmt.Sprintf("%d", mount.SampleRate))
writer.Header().Set("icy-audio-info", fmt.Sprintf("ice-channels=%d;ice-samplerate=%d;ice-bitrate=%d", 2, mount.SampleRate, bitrate))
writer.Header().Set("icy-audio-info", fmt.Sprintf("ice-channels=%d;ice-samplerate=%d;ice-bitrate=%d", mount.Channels, mount.SampleRate, bitrate))
if q.config.Radio.Private {
writer.Header().Set("icy-pub", "0")
writer.Header().Set("icy-do-not-index", "1")
@ -614,7 +658,7 @@ func (q *Queue) HandleRadioRequest(writer http.ResponseWriter, request *http.Req
headerBytes := new(bytes.Buffer)
binary.Write(headerBytes, binary.LittleEndian, int64(2))
binary.Write(headerBytes, binary.LittleEndian, int64(mount.Channels))
binary.Write(headerBytes, binary.LittleEndian, int64(mount.SampleRate))
binary.Write(headerBytes, binary.LittleEndian, int32(len(mount.MimeType)))
headerBytes.Write([]byte(mount.MimeType))