diff options
Diffstat (limited to 'rhimport/wave_generator.go')
-rw-r--r-- | rhimport/wave_generator.go | 221 |
1 files changed, 221 insertions, 0 deletions
diff --git a/rhimport/wave_generator.go b/rhimport/wave_generator.go new file mode 100644 index 0000000..f4c3261 --- /dev/null +++ b/rhimport/wave_generator.go @@ -0,0 +1,221 @@ +// +// rhimportd +// +// The Radio Helsinki Rivendell Import Daemon +// +// +// Copyright (C) 2015-2016 Christian Pointner <equinox@helsinki.at> +// +// This file is part of rhimportd. +// +// rhimportd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// rhimportd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with rhimportd. If not, see <http://www.gnu.org/licenses/>. +// + +package rhimport + +import ( + "bytes" + "encoding/binary" + "errors" + "io" + "math" + "time" +) + +const ( + RIFF_TAG = "RIFF" + WAVE_TAG = "WAVE" + FMT_TAG = "fmt " + FMT_ID_PCM = 0x0001 + DATA_TAG = "data" +) + +type wavHeader struct { + riffTag [4]uint8 + riffLength uint32 + waveTag [4]uint8 + + fmtTag [4]uint8 + fmtLength uint32 + fmtID uint16 + nChannels uint16 + sampleRate uint32 + byteRate uint32 + blockAlign uint16 + sampleDepth uint16 + + dataTag [4]uint8 + dataLength uint32 +} + +func (h *wavHeader) Bytes() []byte { + buf := new(bytes.Buffer) + binary.Write(buf, binary.LittleEndian, h) + return buf.Bytes() +} + +type SampleGenerator interface { + Reset(samplePeriod float64) + GetSamples(nSamples uint32, channels uint16) []float64 // this needs to be normalized, aka -1 <= value <= 1 +} + +type WavFile struct { + header wavHeader + headerSize uint32 + pcmSampleMax float64 + pcmSampleBytes uint32 + samplePeriod float64 + Generator SampleGenerator + readOffset uint32 +} + +func (wav *WavFile) GetFileSize() (size uint32) { + return wav.headerSize + wav.header.dataLength +} + +func (wav *WavFile) Read(p []byte) (n int, err error) { + n = 0 + if wav.readOffset >= (wav.header.riffLength + 8) { + return n, io.EOF + } + + if wav.readOffset < wav.headerSize { + n = copy(p, wav.header.Bytes()[wav.readOffset:]) + wav.readOffset += uint32(n) + wav.Generator.Reset(wav.samplePeriod) + } + if n >= len(p) { + return + } + nsamples := uint32(len(p)-n) / uint32(wav.header.blockAlign) + data := wav.Generator.GetSamples(nsamples, wav.header.nChannels) + + switch wav.header.fmtID { + case FMT_ID_PCM: + idx := 0 + for _, normalized := range data { + scaled := wav.pcmSampleMax * normalized + sample := uint64(math.Trunc(math.Min(scaled, wav.pcmSampleMax))) + var b [8]byte + binary.LittleEndian.PutUint64(b[:], sample) + copy(p[n:n+int(wav.pcmSampleBytes)], b[:]) + n += int(wav.pcmSampleBytes) + wav.readOffset += wav.pcmSampleBytes + if wav.readOffset >= (wav.header.riffLength + 8) { + return n, io.EOF + } + idx++ + } + default: + return n, errors.New("unknown sample format ID") + } + return +} + +func NewPCMWavFile(sampleRate uint32, sampleDepth uint16, channels uint16, length time.Duration) (wav *WavFile, err error) { + wav = &WavFile{} + wav.headerSize = 8 + 4 + 8 + 16 + 8 + if length <= 0 { + return nil, errors.New("invalid length: must be > 0") + } + wav.pcmSampleMax = float64(uint64(1<<(sampleDepth-1)) - 1) + wav.pcmSampleBytes = ((uint32(sampleDepth) + 7) / 8) + wav.samplePeriod = 1.0 / float64(sampleRate) + + frameSize32 := uint32(channels) * wav.pcmSampleBytes + if frameSize32 > math.MaxUint16 { + return nil, errors.New("frame size exceeds 16bit values (64kB)") + } + frameSize := uint16(frameSize32) + + period_ns := float64(time.Second) / float64(sampleRate) + nFramesF64 := (float64(length) / period_ns) + if nFramesF64 < 1 { + nFramesF64 = 1 + } + if nFramesF64 > math.MaxUint32 { + return nil, errors.New("number of frames exceeds limit (reduce length and/or sample-rate)") + } + + nFrames := uint32(nFramesF64) + if nFrames > ((math.MaxUint32 - wav.headerSize) / uint32(frameSize)) { + return nil, errors.New("file length exceeds 32bit values (4GB)") + } + dataLen := nFrames * uint32(frameSize) + + copy(wav.header.riffTag[:], RIFF_TAG) + wav.header.riffLength = wav.headerSize - 8 + dataLen + copy(wav.header.waveTag[:], WAVE_TAG) + + copy(wav.header.fmtTag[:], FMT_TAG) + wav.header.fmtLength = 16 + wav.header.fmtID = FMT_ID_PCM + wav.header.nChannels = channels + wav.header.sampleRate = sampleRate + wav.header.byteRate = sampleRate * uint32(frameSize) + wav.header.blockAlign = frameSize + wav.header.sampleDepth = sampleDepth + + copy(wav.header.dataTag[:], DATA_TAG) + wav.header.dataLength = dataLen + + return +} + +type SilenceGenerator struct { +} + +func NewSilenceGenerator() *SilenceGenerator { + return &SilenceGenerator{} +} + +func (s *SilenceGenerator) Reset(samplePeriod float64) { + // nothing here +} + +func (s *SilenceGenerator) GetSamples(nSamples uint32, nChannels uint16) (data []float64) { + data = make([]float64, int(nSamples)*int(nChannels)) + return +} + +type SinusGenerator struct { + amp float64 + freq float64 + sp float64 + t float64 +} + +func NewSinusGenerator(ampDB, freq float64) (sin *SinusGenerator) { + sin = &SinusGenerator{} + sin.amp = math.Pow(10.0, (ampDB / 20.0)) + sin.freq = freq + return +} + +func (sin *SinusGenerator) Reset(samplePeriod float64) { + sin.sp = samplePeriod + sin.t = 0 +} + +func (sin *SinusGenerator) GetSamples(nSamples uint32, nChannels uint16) (data []float64) { + data = make([]float64, int(nSamples)*int(nChannels)) + for i := 0; i < int(nSamples); i++ { + val := sin.amp * math.Sin(2*math.Pi*sin.freq*sin.t) + for j := 0; j < int(nChannels); j++ { + data[i*int(nChannels)+j] = val + } + sin.t += sin.sp + } + return +} |