Commit: 91f6bf7191523ec4cca658314173f7d806dedb6c Author: Vi Grey Date: 2017-07-17 06:34 UTC Summary: Initial commit .gitignore | 2 + LICENSE | 24 ++ Makefile | 42 ++++ README.md | 54 +++++ src/modem-tap-go/io.go | 67 ++++++ src/modem-tap-go/modem-tap-go.go | 218 +++++++++++++++++ src/modem-tap-go/telnet-client.go | 37 +++ src/modem-tap-go/telnet-server.go | 36 +++ src/modem-tap.sh | 3 + vendor/manifest | 11 + vendor/src/github.com/gordonklaus/portaudio/README.md | 7 + vendor/src/github.com/gordonklaus/portaudio/examples/echo.go | 48 ++++ vendor/src/github.com/gordonklaus/portaudio/examples/enumerate.go | 40 ++++ vendor/src/github.com/gordonklaus/portaudio/examples/noise.go | 30 +++ vendor/src/github.com/gordonklaus/portaudio/examples/play.go | 126 ++++++++++ vendor/src/github.com/gordonklaus/portaudio/examples/record.go | 93 ++++++++ vendor/src/github.com/gordonklaus/portaudio/examples/stereoSine.go | 48 ++++ vendor/src/github.com/gordonklaus/portaudio/pa.c | 9 + vendor/src/github.com/gordonklaus/portaudio/portaudio.go | 1016 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 19 files changed, 1911 insertions(+) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5682f9e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +bin +pkg diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7091d86 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +Copyright (C) 2017, Vi Grey +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..be7e9af --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ +# Copyright (C) 2017, Vi Grey +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +PKG_NAME := modem-tap + +all: + gb build all + cp src/$(PKG_NAME).sh bin/$(PKG_NAME) + +clean: + rm -rf bin + +install: + cp bin/$(PKG_NAME) /usr/local/bin/ + cp bin/$(PKG_NAME)-go /usr/local/bin/ + +uninstall: + rm -rf /usr/local/bin/$(PKG_NAME) + rm -rf /usr/local/bin/$(PKG_NAME)-go + diff --git a/README.md b/README.md new file mode 100644 index 0000000..5f6c8b1 --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# modem-tap + +An audio Bell103 300 baud modem wiretap synthesizer + +**_Modem-Tap is created by Vi Grey (https://vigrey.com) and is licensed under the BSD 2-Clause License. Read LICENSE for more license text._** + +#### Description: +**THIS PROJECT IS A PERSONAL PROJECT. I CANNOT PROMISE UPDATES FOR ANY FEATURES OR FIXES OTHER THAN FEATURES AND FIXES I WISH TO ADD. QUITE A BIT OF THE ERROR HANDLING IS SIMPLY JUST ME KNOWING WHAT I SHOULD AND SHOULD NOT DO WITH THIS PROGRAM TO KEEP IT FROM CRASHING AND/OR RUNNING INTO ERRORS.** + +Modem-Tap is a Bell103 300 baud modem noise synthesizer that emulates a 300bps dial-up connection and produces the incoming and outgoing connection data tones. + +This project is called Modem-Tap because it creates the sounds that would be heard if the phone line handling transferring the 300 baud dial-up connection was wiretapped. + +Connections through Modem-Tap are throttled to 300bps and uses 8-N-1 ascii encoding. + +#### Platforms: +- GNU/Linux + +#### Build Dependencies: +- gb +- Go >= 1.8 +- Portaudio-Dev >= 19 + +#### Dependencies: +- telnet + +#### Install: + + $ make + $ sudo make install + +#### Uninstall: + $ sudo make uninstall + +#### Usage: + $ modem-tap [ OPTIONS ]... [ LISTENING PORT ] + + Options: + + -h, --help Print help (this message) and exit + -q, --quiet Does not play connection sounds from the speaker + -w, --wav Saves connection sounds to a WAV file + + LISTENING PORT is 2600 by default + +#### Startup: + Upon startup, Modem-Tap will ask for a server address. It will then ask for a server port. After you supply both the address and the port, Modem-Tap will then listen at localhost on the LISTENING PORT you specified, which is 2600 if no port is specified. You can then telnet into localhost:2600 and Modem-Tap will make a telnet connection out to the server address on the server port. This connection will create the Bell 103 modulation frequency sounds for the incoming and outgoing internet traffic, which can be played through speakers and/or recorded to a WAV file. + + After the connection is closed, if Modem-Tap has not been closed with ctrl-c, it should ask for another server address and then another server port. + +#### WAV Files: + WAV files created by Modem-Tap will have the filename syntax of serveraddress-serverport-YYYYMMDDhhmmss.wav, for instance, a connection to vigrey.com on port 80 could produce the file name vigrey.com-80-20170717011252.wav. This file will be a single channel 44100Hz 16-bit PCM WAV file. + + A single WAV file is created for each connection to a server from Modem-Tap. Multiple connections can be created on a single instance of running Modem-Tap. diff --git a/src/modem-tap-go/io.go b/src/modem-tap-go/io.go new file mode 100644 index 0000000..8bf42e7 --- /dev/null +++ b/src/modem-tap-go/io.go @@ -0,0 +1,67 @@ +package main + +import ( + "bytes" + "encoding/binary" + "io/ioutil" + "log" + "os" + "time" +) + +var ( + pcmOut []byte + homeDir = os.Getenv("HOME") +) + +func makeWav(data []byte) []byte { + wavFile := []byte("RIFF") + subchunk1 := []byte("fmt ") + subchunk1 = append(subchunk1, []byte{16, 0, 0, 0}...) + subchunk1 = append(subchunk1, []byte{1, 0}...) + subchunk1 = append(subchunk1, []byte{1, 0}...) + subchunk1SampleRate := new(bytes.Buffer) + binary.Write(subchunk1SampleRate, binary.LittleEndian, uint32(sampleRate)) + subchunk1 = append(subchunk1, subchunk1SampleRate.Bytes()...) + subchunk1ByteRate := new(bytes.Buffer) + binary.Write(subchunk1ByteRate, binary.LittleEndian, uint32(sampleRate * 2)) + subchunk1 = append(subchunk1, subchunk1ByteRate.Bytes()...) + subchunk1 = append(subchunk1, []byte{2, 0}...) + subchunk1 = append(subchunk1, []byte{16, 0}...) + subchunk2 := []byte("data") + subchunk2Len := new(bytes.Buffer) + binary.Write(subchunk2Len, binary.LittleEndian, uint32(len(data))) + subchunk2 = append(subchunk2, subchunk2Len.Bytes()...) + subchunk2 = append(subchunk2, data...) + subchunksLen := new(bytes.Buffer) + binary.Write(subchunksLen, binary.LittleEndian, uint32(len(subchunk1) + + len(subchunk2) + 4)) + wavFile = append(wavFile, subchunksLen.Bytes()...) + wavFile = append(wavFile, []byte("WAVE")...) + wavFile = append(wavFile, subchunk1...) + wavFile = append(wavFile, subchunk2...) + return wavFile +} + +func setupWavFolder() { + if _, err := os.Stat(homeDir + "/Modem-Tap"); os.IsNotExist(err) { + err := os.Mkdir(homeDir + "/Modem-Tap", 0755) + if err != nil { + log.Println("\x1b[91mmodem-tap: Unable to make dir " + homeDir + + "/Modem-Tap\x1b[0m") + os.Exit(1) + } + } +} + +func writeWav(data []byte) { + t := time.Now() + wavFile := makeWav(data) + setupWavFolder() + err := ioutil.WriteFile(homeDir + "/Modem-Tap/" + dialAddress + "-" + + dialPort + "-" + t.Format("20060102150405") + ".wav", + wavFile, 0644) + if err != nil { + panic(err) + } +} diff --git a/src/modem-tap-go/modem-tap-go.go b/src/modem-tap-go/modem-tap-go.go new file mode 100644 index 0000000..9d06b80 --- /dev/null +++ b/src/modem-tap-go/modem-tap-go.go @@ -0,0 +1,218 @@ +package main + +import ( + "github.com/gordonklaus/portaudio" + "bytes" + "encoding/binary" + "fmt" + "math" + "os" + "strconv" +) + +const sampleRate = 44100 +const pcmScale = 32768 +const baud = 147 +const scale = 0.5 + +var ( + baudOutBuffer, baudInBuffer []bool + writeBuffer, readBuffer []byte + baudOutTick, baudInTick, writeTick, readTick int + dialAddress, dialPort string + listenPort = "2600" + quietFlag, wavFlag bool +) + +type sine struct { + *portaudio.Stream + phaseOut, phaseIn float64 +} + +func Reverse(s string) string { + runes := []rune(s) + for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { + runes[i], runes[j] = runes[j], runes[i] + } + return string(runes) +} + +func bytesToBuffer8N1(word []byte) (buffer []bool) { + for _, n := range word { + a := strconv.FormatInt(int64(n), 2) + tmpA := a + for i := 0; i < 8 - len(a); i++ { + tmpA = "0" + tmpA + } + a = tmpA + buffer = append(buffer, false) + for _, b := range Reverse(a) { + if b == '1' { + buffer = append(buffer, true) + } else { + buffer = append(buffer, false) + } + } + buffer = append(buffer, true) + } + return +} + +func newSine(sampleRate float64) *sine { + s := &sine{nil, 0, 0} + var err error + s.Stream, err = portaudio.OpenDefaultStream(0, 1, sampleRate, 0, s.processAudio) + chk(err) + return s +} + +func (g *sine) processAudio(out []float32) { + for i := range out { + var freqOut float64 = 1270 + var freqIn float64 = 2225 + if telnetIn == nil { + baudOutTick = 0 + writeTick = 0 + baudOutBuffer = []bool{} + writeBuffer = []byte{} + freqOut = 0 + } + if telnetOut == nil { + baudInTick = 0 + readTick = 0 + baudInBuffer = []bool{} + readBuffer = []byte{} + freqIn = 0 + } + bitOut := true + bitIn := true + if len(baudOutBuffer) > 0 { + if baudOutTick != baud { + baudOutTick++ + if baudOutTick == baud { + if len(baudOutBuffer) > 0 { + baudOutBuffer = baudOutBuffer[1:] + writeTick++ + if writeTick == 10 { + writeTick = 0 + if len(writeBuffer) > 0 { + telnetOut.Write([]byte{writeBuffer[0]}) + writeBuffer = writeBuffer[1:] + } + } + } + } + } else { + baudOutTick = 0 + } + if len(baudOutBuffer) > 0 { + if baudOutBuffer[0] == false { + bitOut = false + } + } + } + if len(baudInBuffer) > 0 { + if baudInTick != baud { + baudInTick++ + if baudInTick == baud { + if len(baudInBuffer) > 0 { + baudInBuffer = baudInBuffer[1:] + readTick++ + if readTick == 10 { + readTick = 0 + if len(readBuffer) > 0 { + telnetIn.Write([]byte{readBuffer[0]}) + readBuffer = readBuffer[1:] + } + } + } + } + } else { + baudInTick = 0 + } + if len(baudInBuffer) > 0 { + if baudInBuffer[0] == false { + bitIn = false + } + } + } + if !bitOut { + freqOut = 1070 + } + if !bitIn { + freqIn = 2025 + } + stepOut := freqOut / sampleRate + stepIn := freqIn / sampleRate + tmpOut := float32(math.Sin(2 * math.Pi * g.phaseOut) * scale + + math.Sin(2 * math.Pi * g.phaseIn) * scale) + if quietFlag { + out[i] = 0 + } else{ + out[i] = tmpOut + } + if freqOut != 0 && freqIn != 0 && wavFlag { + pcmBuf16 := new(bytes.Buffer) + binary.Write(pcmBuf16, binary.LittleEndian, int16(tmpOut * pcmScale)) + pcmOut = append(pcmOut, pcmBuf16.Bytes()...) + } + if freqOut == 0 && freqIn == 0 && wavFlag { + if len(pcmOut) > 0 { + pcmOutCopy := pcmOut + pcmOut = []byte{} + writeWav(pcmOutCopy) + } + } + _, g.phaseOut = math.Modf(g.phaseOut + stepOut) + _, g.phaseIn = math.Modf(g.phaseIn + stepIn) + } +} + +func chk(err error) { + if err != nil { + panic(err) + } +} + +func main() { + if len(os.Args) > 1 { + for i, arg := range os.Args[1:] { + if arg == "-h" || arg == "--help" { + fmt.Println("modem-tap [ OPTIONS ]... [ LISTENING PORT ]\n\n" + + "Options:\n\n" + + "-h, --help Print help (this message) and exit\n" + + "-q, --quiet Does not play connection sounds from " + + "the speaker\n" + + "-w, --wav Saves connection sounds to a WAV file\n\n" + + "LISTENING PORT is 2600 by default") + os.Exit(0) + } else if arg == "-w" || arg == "--wav" { + wavFlag = true + } else if arg == "-q" || arg == "--quiet"{ + quietFlag = true + } else if i + 1 == len(os.Args) - 1 { + listenPort = arg + } + } + } + portaudio.Initialize() + defer portaudio.Terminate() + s := newSine(sampleRate) + defer s.Close() + chk(s.Start()) + for { + var tmpDialAddress, tmpDialPort string + fmt.Printf("\x1b[92mEnter Server Address:\x1b[0m ") + fmt.Scanln(&tmpDialAddress) + dialAddress = tmpDialAddress + fmt.Printf("\x1b[92mEnter Port [23]:\x1b[0m ") + fmt.Scanln(&tmpDialPort) + dialPort = tmpDialPort + if dialPort == "" { + dialPort = "23" + } + server() + } + chk(s.Stop()) +} + diff --git a/src/modem-tap-go/telnet-client.go b/src/modem-tap-go/telnet-client.go new file mode 100644 index 0000000..7e694d9 --- /dev/null +++ b/src/modem-tap-go/telnet-client.go @@ -0,0 +1,37 @@ +package main + +import ( + "net" + "time" +) + +var ( + telnetOut net.Conn +) + +func client() { + // connect to this port + telnetOut, _ = net.Dial("tcp", dialAddress + ":" + dialPort) + for { + buf := make([]byte, 1024) + l, err := telnetOut.Read(buf) + if err != nil { + break + } + res := buf[:l] + go func() { + // Insert artificial 200ms round trip latency for added authenticity + time.Sleep(200 * time.Millisecond) + baudInBuffer = append(baudInBuffer, bytesToBuffer8N1(res)...) + readBuffer = append(readBuffer, res...) + }() + } + if telnetOut != nil { + telnetOut.Close() + telnetOut = nil + } + if telnetIn != nil { + telnetIn.Close() + telnetIn = nil + } +} diff --git a/src/modem-tap-go/telnet-server.go b/src/modem-tap-go/telnet-server.go new file mode 100644 index 0000000..4d6d848 --- /dev/null +++ b/src/modem-tap-go/telnet-server.go @@ -0,0 +1,36 @@ +package main + +import ( + "fmt" + "net" +) + +var ( + telnetIn net.Conn +) + +func server() { + ln, _ := net.Listen("tcp", "localhost:" + listenPort) + fmt.Printf("\x1b[94mListening on localhost:" + listenPort + "\x1b[0m\n") + telnetIn, _ = ln.Accept() + go client() + defer ln.Close() + for { + buf := make([]byte, 1024) + l, err := telnetIn.Read(buf) + if err != nil { + break + } + req := buf[:l] + writeBuffer = append(writeBuffer, req...) + baudOutBuffer = append(baudOutBuffer, bytesToBuffer8N1(req)...) + } + if telnetIn != nil { + telnetIn.Close() + telnetIn = nil + } + if telnetOut != nil { + telnetOut.Close() + telnetOut = nil + } +} diff --git a/src/modem-tap.sh b/src/modem-tap.sh new file mode 100644 index 0000000..149c78a --- /dev/null +++ b/src/modem-tap.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +modem-tap-go $@ 2>/dev/null diff --git a/vendor/manifest b/vendor/manifest new file mode 100644 index 0000000..d7c2ef2 --- /dev/null +++ b/vendor/manifest @@ -0,0 +1,11 @@ +{ + "version": 0, + "dependencies": [ + { + "importpath": "github.com/gordonklaus/portaudio", + "repository": "https://github.com/gordonklaus/portaudio", + "revision": "e3db054b5758f438a1322fc1712f11074fad107a", + "branch": "master" + } + ] +} diff --git a/vendor/src/github.com/gordonklaus/portaudio/README.md b/vendor/src/github.com/gordonklaus/portaudio/README.md new file mode 100644 index 0000000..66902a3 --- /dev/null +++ b/vendor/src/github.com/gordonklaus/portaudio/README.md @@ -0,0 +1,7 @@ +# portaudio + +This package provides an interface to the [PortAudio](http://www.portaudio.com/) audio I/O library. See the [package documentation](http://godoc.org/github.com/gordonklaus/portaudio) for details. + +To build this package you must first have the PortAudio development headers and libraries installed. Some systems provide a package for this; e.g., on Ubuntu you would want to run `apt-get install portaudio19-dev`. On other systems you might have to install from source. + +Thanks to sqweek for motivating and contributing to host API and device enumeration. diff --git a/vendor/src/github.com/gordonklaus/portaudio/examples/echo.go b/vendor/src/github.com/gordonklaus/portaudio/examples/echo.go new file mode 100644 index 0000000..3281d45 --- /dev/null +++ b/vendor/src/github.com/gordonklaus/portaudio/examples/echo.go @@ -0,0 +1,48 @@ +package main + +import ( + "github.com/gordonklaus/portaudio" + "time" +) + +func main() { + portaudio.Initialize() + defer portaudio.Terminate() + e := newEcho(time.Second / 3) + defer e.Close() + chk(e.Start()) + time.Sleep(4 * time.Second) + chk(e.Stop()) +} + +type echo struct { + *portaudio.Stream + buffer []float32 + i int +} + +func newEcho(delay time.Duration) *echo { + h, err := portaudio.DefaultHostApi() + chk(err) + p := portaudio.LowLatencyParameters(h.DefaultInputDevice, h.DefaultOutputDevice) + p.Input.Channels = 1 + p.Output.Channels = 1 + e := &echo{buffer: make([]float32, int(p.SampleRate*delay.Seconds()))} + e.Stream, err = portaudio.OpenStream(p, e.processAudio) + chk(err) + return e +} + +func (e *echo) processAudio(in, out []float32) { + for i := range out { + out[i] = .7 * e.buffer[e.i] + e.buffer[e.i] = in[i] + e.i = (e.i + 1) % len(e.buffer) + } +} + +func chk(err error) { + if err != nil { + panic(err) + } +} diff --git a/vendor/src/github.com/gordonklaus/portaudio/examples/enumerate.go b/vendor/src/github.com/gordonklaus/portaudio/examples/enumerate.go new file mode 100644 index 0000000..85d85f7 --- /dev/null +++ b/vendor/src/github.com/gordonklaus/portaudio/examples/enumerate.go @@ -0,0 +1,40 @@ +package main + +import ( + "github.com/gordonklaus/portaudio" + "os" + "text/template" +) + +var tmpl = template.Must(template.New("").Parse( + `{{. | len}} host APIs: {{range .}} + Name: {{.Name}} + {{if .DefaultInputDevice}}Default input device: {{.DefaultInputDevice.Name}}{{end}} + {{if .DefaultOutputDevice}}Default output device: {{.DefaultOutputDevice.Name}}{{end}} + Devices: {{range .Devices}} + Name: {{.Name}} + MaxInputChannels: {{.MaxInputChannels}} + MaxOutputChannels: {{.MaxOutputChannels}} + DefaultLowInputLatency: {{.DefaultLowInputLatency}} + DefaultLowOutputLatency: {{.DefaultLowOutputLatency}} + DefaultHighInputLatency: {{.DefaultHighInputLatency}} + DefaultHighOutputLatency: {{.DefaultHighOutputLatency}} + DefaultSampleRate: {{.DefaultSampleRate}} + {{end}} +{{end}}`, +)) + +func main() { + portaudio.Initialize() + defer portaudio.Terminate() + hs, err := portaudio.HostApis() + chk(err) + err = tmpl.Execute(os.Stdout, hs) + chk(err) +} + +func chk(err error) { + if err != nil { + panic(err) + } +} diff --git a/vendor/src/github.com/gordonklaus/portaudio/examples/noise.go b/vendor/src/github.com/gordonklaus/portaudio/examples/noise.go new file mode 100644 index 0000000..f9d4798 --- /dev/null +++ b/vendor/src/github.com/gordonklaus/portaudio/examples/noise.go @@ -0,0 +1,30 @@ +package main + +import ( + "github.com/gordonklaus/portaudio" + "math/rand" + "time" +) + +func main() { + portaudio.Initialize() + defer portaudio.Terminate() + h, err := portaudio.DefaultHostApi() + chk(err) + stream, err := portaudio.OpenStream(portaudio.HighLatencyParameters(nil, h.DefaultOutputDevice), func(out []int32) { + for i := range out { + out[i] = int32(rand.Uint32()) + } + }) + chk(err) + defer stream.Close() + chk(stream.Start()) + time.Sleep(time.Second) + chk(stream.Stop()) +} + +func chk(err error) { + if err != nil { + panic(err) + } +} diff --git a/vendor/src/github.com/gordonklaus/portaudio/examples/play.go b/vendor/src/github.com/gordonklaus/portaudio/examples/play.go new file mode 100644 index 0000000..137fd86 --- /dev/null +++ b/vendor/src/github.com/gordonklaus/portaudio/examples/play.go @@ -0,0 +1,126 @@ +package main + +import ( + "github.com/gordonklaus/portaudio" + "encoding/binary" + "fmt" + "io" + "os" + "os/signal" +) + +func main() { + if len(os.Args) < 2 { + fmt.Println("missing required argument: input file name") + return + } + fmt.Println("Playing. Press Ctrl-C to stop.") + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, os.Kill) + + fileName := os.Args[1] + f, err := os.Open(fileName) + chk(err) + defer f.Close() + + id, data, err := readChunk(f) + chk(err) + if id.String() != "FORM" { + fmt.Println("bad file format") + return + } + _, err = data.Read(id[:]) + chk(err) + if id.String() != "AIFF" { + fmt.Println("bad file format") + return + } + var c commonChunk + var audio io.Reader + for { + id, chunk, err := readChunk(data) + if err == io.EOF { + break + } + chk(err) + switch id.String() { + case "COMM": + chk(binary.Read(chunk, binary.BigEndian, &c)) + case "SSND": + chunk.Seek(8, 1) //ignore offset and block + audio = chunk + default: + fmt.Printf("ignoring unknown chunk '%s'\n", id) + } + } + + //assume 44100 sample rate, mono, 32 bit + + portaudio.Initialize() + defer portaudio.Terminate() + out := make([]int32, 8192) + stream, err := portaudio.OpenDefaultStream(0, 1, 44100, len(out), &out) + chk(err) + defer stream.Close() + + chk(stream.Start()) + defer stream.Stop() + for remaining := int(c.NumSamples); remaining > 0; remaining -= len(out) { + if len(out) > remaining { + out = out[:remaining] + } + err := binary.Read(audio, binary.BigEndian, out) + if err == io.EOF { + break + } + chk(err) + chk(stream.Write()) + select { + case <-sig: + return + default: + } + } +} + +func readChunk(r readerAtSeeker) (id ID, data *io.SectionReader, err error) { + _, err = r.Read(id[:]) + if err != nil { + return + } + var n int32 + err = binary.Read(r, binary.BigEndian, &n) + if err != nil { + return + } + off, _ := r.Seek(0, 1) + data = io.NewSectionReader(r, off, int64(n)) + _, err = r.Seek(int64(n), 1) + return +} + +type readerAtSeeker interface { + io.Reader + io.ReaderAt + io.Seeker +} + +type ID [4]byte + +func (id ID) String() string { + return string(id[:]) +} + +type commonChunk struct { + NumChans int16 + NumSamples int32 + BitsPerSample int16 + SampleRate [10]byte +} + +func chk(err error) { + if err != nil { + panic(err) + } +} diff --git a/vendor/src/github.com/gordonklaus/portaudio/examples/record.go b/vendor/src/github.com/gordonklaus/portaudio/examples/record.go new file mode 100644 index 0000000..f5e8349 --- /dev/null +++ b/vendor/src/github.com/gordonklaus/portaudio/examples/record.go @@ -0,0 +1,93 @@ +package main + +import ( + "github.com/gordonklaus/portaudio" + "encoding/binary" + "fmt" + "os" + "os/signal" + "strings" +) + +func main() { + if len(os.Args) < 2 { + fmt.Println("missing required argument: output file name") + return + } + fmt.Println("Recording. Press Ctrl-C to stop.") + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, os.Kill) + + fileName := os.Args[1] + if !strings.HasSuffix(fileName, ".aiff") { + fileName += ".aiff" + } + f, err := os.Create(fileName) + chk(err) + + // form chunk + _, err = f.WriteString("FORM") + chk(err) + chk(binary.Write(f, binary.BigEndian, int32(0))) //total bytes + _, err = f.WriteString("AIFF") + chk(err) + + // common chunk + _, err = f.WriteString("COMM") + chk(err) + chk(binary.Write(f, binary.BigEndian, int32(18))) //size + chk(binary.Write(f, binary.BigEndian, int16(1))) //channels + chk(binary.Write(f, binary.BigEndian, int32(0))) //number of samples + chk(binary.Write(f, binary.BigEndian, int16(32))) //bits per sample + _, err = f.Write([]byte{0x40, 0x0e, 0xac, 0x44, 0, 0, 0, 0, 0, 0}) //80-bit sample rate 44100 + chk(err) + + // sound chunk + _, err = f.WriteString("SSND") + chk(err) + chk(binary.Write(f, binary.BigEndian, int32(0))) //size + chk(binary.Write(f, binary.BigEndian, int32(0))) //offset + chk(binary.Write(f, binary.BigEndian, int32(0))) //block + nSamples := 0 + defer func() { + // fill in missing sizes + totalBytes := 4 + 8 + 18 + 8 + 8 + 4*nSamples + _, err = f.Seek(4, 0) + chk(err) + chk(binary.Write(f, binary.BigEndian, int32(totalBytes))) + _, err = f.Seek(22, 0) + chk(err) + chk(binary.Write(f, binary.BigEndian, int32(nSamples))) + _, err = f.Seek(42, 0) + chk(err) + chk(binary.Write(f, binary.BigEndian, int32(4*nSamples+8))) + chk(f.Close()) + }() + + portaudio.Initialize() + defer portaudio.Terminate() + in := make([]int32, 64) + stream, err := portaudio.OpenDefaultStream(1, 0, 44100, len(in), in) + chk(err) + defer stream.Close() + + chk(stream.Start()) + for { + chk(stream.Read()) + chk(binary.Write(f, binary.BigEndian, in)) + nSamples += len(in) + select { + case <-sig: + return + default: + } + } + chk(stream.Stop()) +} + +func chk(err error) { + if err != nil { + panic(err) + } +} diff --git a/vendor/src/github.com/gordonklaus/portaudio/examples/stereoSine.go b/vendor/src/github.com/gordonklaus/portaudio/examples/stereoSine.go new file mode 100644 index 0000000..a8364cc --- /dev/null +++ b/vendor/src/github.com/gordonklaus/portaudio/examples/stereoSine.go @@ -0,0 +1,48 @@ +package main + +import ( + "github.com/gordonklaus/portaudio" + "math" + "time" +) + +const sampleRate = 44100 + +func main() { + portaudio.Initialize() + defer portaudio.Terminate() + s := newStereoSine(256, 320, sampleRate) + defer s.Close() + chk(s.Start()) + time.Sleep(2 * time.Second) + chk(s.Stop()) +} + +type stereoSine struct { + *portaudio.Stream + stepL, phaseL float64 + stepR, phaseR float64 +} + +func newStereoSine(freqL, freqR, sampleRate float64) *stereoSine { + s := &stereoSine{nil, freqL / sampleRate, 0, freqR / sampleRate, 0} + var err error + s.Stream, err = portaudio.OpenDefaultStream(0, 2, sampleRate, 0, s.processAudio) + chk(err) + return s +} + +func (g *stereoSine) processAudio(out [][]float32) { + for i := range out[0] { + out[0][i] = float32(math.Sin(2 * math.Pi * g.phaseL)) + _, g.phaseL = math.Modf(g.phaseL + g.stepL) + out[1][i] = float32(math.Sin(2 * math.Pi * g.phaseR)) + _, g.phaseR = math.Modf(g.phaseR + g.stepR) + } +} + +func chk(err error) { + if err != nil { + panic(err) + } +} diff --git a/vendor/src/github.com/gordonklaus/portaudio/pa.c b/vendor/src/github.com/gordonklaus/portaudio/pa.c new file mode 100644 index 0000000..d383504 --- /dev/null +++ b/vendor/src/github.com/gordonklaus/portaudio/pa.c @@ -0,0 +1,9 @@ +#include "_cgo_export.h" + +int cb(const void *inputBuffer, void *outputBuffer, unsigned long frames, const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags, void *userData) { + streamCallback((void*)inputBuffer, outputBuffer, frames, (PaStreamCallbackTimeInfo*)timeInfo, statusFlags, userData); + return paContinue; +} + +//using a variable ensures that the callback signature is checked +PaStreamCallback* paStreamCallback = cb; diff --git a/vendor/src/github.com/gordonklaus/portaudio/portaudio.go b/vendor/src/github.com/gordonklaus/portaudio/portaudio.go new file mode 100644 index 0000000..a2830a6 --- /dev/null +++ b/vendor/src/github.com/gordonklaus/portaudio/portaudio.go @@ -0,0 +1,1016 @@ +/* +Package portaudio applies Go bindings to the PortAudio library. + +For the most part, these bindings parallel the underlying PortAudio API; please refer to http://www.portaudio.com/docs.html for details. Differences introduced by the bindings are documented here: + +Instead of passing a flag to OpenStream, audio sample formats are inferred from the signature of the stream callback or, for a blocking stream, from the types of the buffers. See the StreamCallback and Buffer types for details. + +Blocking I/O: Read and Write do not accept buffer arguments; instead they use the buffers (or pointers to buffers) provided to OpenStream. The number of samples to read or write is determined by the size of the buffers. + +The StreamParameters struct combines parameters for both the input and the output device as well as the sample rate, buffer size, and flags. +*/ +package portaudio + +/* +#cgo pkg-config: portaudio-2.0 +#include +extern PaStreamCallback* paStreamCallback; +*/ +import "C" + +import ( + "fmt" + "os" + "reflect" + "runtime" + "sync" + "time" + "unsafe" +) + +// Version returns the release number of PortAudio. +func Version() int { + return int(C.Pa_GetVersion()) +} + +// VersionText returns the textual description of the PortAudio release. +func VersionText() string { + return C.GoString(C.Pa_GetVersionText()) +} + +// Error wraps over PaError. +type Error C.PaError + +func (err Error) Error() string { + return C.GoString(C.Pa_GetErrorText(C.PaError(err))) +} + +// PortAudio Errors. +const ( + NotInitialized Error = C.paNotInitialized + InvalidChannelCount Error = C.paInvalidChannelCount + InvalidSampleRate Error = C.paInvalidSampleRate + InvalidDevice Error = C.paInvalidDevice + InvalidFlag Error = C.paInvalidFlag + SampleFormatNotSupported Error = C.paSampleFormatNotSupported + BadIODeviceCombination Error = C.paBadIODeviceCombination + InsufficientMemory Error = C.paInsufficientMemory + BufferTooBig Error = C.paBufferTooBig + BufferTooSmall Error = C.paBufferTooSmall + NullCallback Error = C.paNullCallback + BadStreamPtr Error = C.paBadStreamPtr + TimedOut Error = C.paTimedOut + InternalError Error = C.paInternalError + DeviceUnavailable Error = C.paDeviceUnavailable + IncompatibleHostApiSpecificStreamInfo Error = C.paIncompatibleHostApiSpecificStreamInfo + StreamIsStopped Error = C.paStreamIsStopped + StreamIsNotStopped Error = C.paStreamIsNotStopped + InputOverflowed Error = C.paInputOverflowed + OutputUnderflowed Error = C.paOutputUnderflowed + HostApiNotFound Error = C.paHostApiNotFound + InvalidHostApi Error = C.paInvalidHostApi + CanNotReadFromACallbackStream Error = C.paCanNotReadFromACallbackStream + CanNotWriteToACallbackStream Error = C.paCanNotWriteToACallbackStream + CanNotReadFromAnOutputOnlyStream Error = C.paCanNotReadFromAnOutputOnlyStream + CanNotWriteToAnInputOnlyStream Error = C.paCanNotWriteToAnInputOnlyStream + IncompatibleStreamHostApi Error = C.paIncompatibleStreamHostApi + BadBufferPtr Error = C.paBadBufferPtr +) + +// UnanticipatedHostError contains details for ApiHost related errors. +type UnanticipatedHostError struct { + HostApiType HostApiType + Code int + Text string +} + +func (err UnanticipatedHostError) Error() string { + return err.Text +} + +func newError(err C.PaError) error { + switch err { + case C.paUnanticipatedHostError: + hostErr := C.Pa_GetLastHostErrorInfo() + return UnanticipatedHostError{ + HostApiType(hostErr.hostApiType), + int(hostErr.errorCode), + C.GoString(hostErr.errorText), + } + case C.paNoError: + return nil + } + return Error(err) +} + +var initialized = 0 + +// Initialize initializes internal data structures and +// prepares underlying host APIs for use. With the exception +// of Version(), VersionText(), and ErrorText(), this function +// MUST be called before using any other PortAudio API functions. +// +// If Initialize() is called multiple times, each successful call +// must be matched with a corresponding call to Terminate(). Pairs of +// calls to Initialize()/Terminate() may overlap, and are not required to be fully nested. +// +// Note that if Initialize() returns an error code, Terminate() should NOT be called. +func Initialize() error { + paErr := C.Pa_Initialize() + if paErr != C.paNoError { + return newError(paErr) + } + initialized++ + return nil +} + +// Terminate deallocates all resources allocated by PortAudio +// since it was initialized by a call to Initialize(). +// +// In cases where Initialize() has been called multiple times, +// each call must be matched with a corresponding call to Pa_Terminate(). +// The final matching call to Pa_Terminate() will automatically +// close any PortAudio streams that are still open.. +// +// Terminate MUST be called before exiting a program which uses PortAudio. +// Failure to do so may result in serious resource leaks, such as audio devices +// not being available until the next reboot. +func Terminate() error { + paErr := C.Pa_Terminate() + if paErr != C.paNoError { + return newError(paErr) + } + initialized-- + if initialized <= 0 { + initialized = 0 + cached = false + } + return nil +} + +// HostApiType maps ints to HostApi modes. +type HostApiType int + +func (t HostApiType) String() string { + return hostApiStrings[t] +} + +var hostApiStrings = [...]string{ + InDevelopment: "InDevelopment", + DirectSound: "DirectSound", + MME: "MME", + ASIO: "ASIO", + SoundManager: "SoundManager", + CoreAudio: "CoreAudio", + OSS: "OSS", + ALSA: "ALSA", + AL: "AL", + BeOS: "BeOS", + WDMkS: "WDMKS", + JACK: "JACK", + WASAPI: "WASAPI", + AudioScienceHPI: "AudioScienceHPI", +} + +// PortAudio Api types. +const ( + InDevelopment HostApiType = C.paInDevelopment + DirectSound HostApiType = C.paDirectSound + MME HostApiType = C.paMME + ASIO HostApiType = C.paASIO + SoundManager HostApiType = C.paSoundManager + CoreAudio HostApiType = C.paCoreAudio + OSS HostApiType = C.paOSS + ALSA HostApiType = C.paALSA + AL HostApiType = C.paAL + BeOS HostApiType = C.paBeOS + WDMkS HostApiType = C.paWDMKS + JACK HostApiType = C.paJACK + WASAPI HostApiType = C.paWASAPI + AudioScienceHPI HostApiType = C.paAudioScienceHPI +) + +// HostApiInfo contains information for a HostApi. +type HostApiInfo struct { + Type HostApiType + Name string + DefaultInputDevice *DeviceInfo + DefaultOutputDevice *DeviceInfo + Devices []*DeviceInfo +} + +// DeviceInfo contains information for an audio device. +type DeviceInfo struct { + index C.PaDeviceIndex + Name string + MaxInputChannels int + MaxOutputChannels int + DefaultLowInputLatency time.Duration + DefaultLowOutputLatency time.Duration + DefaultHighInputLatency time.Duration + DefaultHighOutputLatency time.Duration + DefaultSampleRate float64 + HostApi *HostApiInfo +} + +// HostApis returns all information available for HostApis. +func HostApis() ([]*HostApiInfo, error) { + hosts, _, err := hostsAndDevices() + if err != nil { + return nil, err + } + return hosts, nil +} + +// HostApi returns information for a requested HostApiType. +func HostApi(apiType HostApiType) (*HostApiInfo, error) { + hosts, err := HostApis() + if err != nil { + return nil, err + } + i := C.Pa_HostApiTypeIdToHostApiIndex(C.PaHostApiTypeId(apiType)) + if i < 0 { + return nil, newError(C.PaError(i)) + } + return hosts[i], nil +} + +// DefaultHostApi returns information of the default HostApi available on the system. +// +// The default host API will be the lowest common denominator host API +// on the current platform and is unlikely to provide the best performance. +func DefaultHostApi() (*HostApiInfo, error) { + hosts, err := HostApis() + if err != nil { + return nil, err + } + i := C.Pa_GetDefaultHostApi() + if i < 0 { + return nil, newError(C.PaError(i)) + } + return hosts[i], nil +} + +// Devices returns information for all available devices on the system. +func Devices() ([]*DeviceInfo, error) { + _, devs, err := hostsAndDevices() + if err != nil { + return nil, err + } + return devs, nil +} + +// DefaultInputDevice returns information for the default +// input device on the system. +func DefaultInputDevice() (*DeviceInfo, error) { + devs, err := Devices() + if err != nil { + return nil, err + } + i := C.Pa_GetDefaultInputDevice() + if i < 0 { + return nil, newError(C.PaError(i)) + } + return devs[i], nil +} + +// DefaultOutputDevice returns information for the default +// output device on the system. +func DefaultOutputDevice() (*DeviceInfo, error) { + devs, err := Devices() + if err != nil { + return nil, err + } + i := C.Pa_GetDefaultOutputDevice() + if i < 0 { + return nil, newError(C.PaError(i)) + } + return devs[i], nil +} + +/* +Cache the HostApi/Device list to simplify the enumeration code. +Note that portaudio itself caches the lists, so these won't go stale. + +However, there is talk of extending the portaudio API to allow clients +to rescan available devices without calling Pa_Terminate() followed by +Pa_Initialize() - our caching strategy will have to change if this +goes ahead. See https://www.assembla.com/spaces/portaudio/tickets/11 +*/ +var ( + cached bool + hostApis []*HostApiInfo + devices []*DeviceInfo +) + +func hostsAndDevices() ([]*HostApiInfo, []*DeviceInfo, error) { + if !cached { + nhosts := C.Pa_GetHostApiCount() + ndevs := C.Pa_GetDeviceCount() + if nhosts < 0 { + return nil, nil, newError(C.PaError(nhosts)) + } + if ndevs < 0 { + return nil, nil, newError(C.PaError(ndevs)) + } + devices = make([]*DeviceInfo, ndevs) + hosti := make([]C.PaHostApiIndex, ndevs) + for i := range devices { + i := C.PaDeviceIndex(i) + paDev := C.Pa_GetDeviceInfo(i) + devices[i] = &DeviceInfo{ + index: i, + Name: C.GoString(paDev.name), + MaxInputChannels: int(paDev.maxInputChannels), + MaxOutputChannels: int(paDev.maxOutputChannels), + DefaultLowInputLatency: duration(paDev.defaultLowInputLatency), + DefaultLowOutputLatency: duration(paDev.defaultLowOutputLatency), + DefaultHighInputLatency: duration(paDev.defaultHighInputLatency), + DefaultHighOutputLatency: duration(paDev.defaultHighOutputLatency), + DefaultSampleRate: float64(paDev.defaultSampleRate), + } + hosti[i] = paDev.hostApi + } + hostApis = make([]*HostApiInfo, nhosts) + for i := range hostApis { + i := C.PaHostApiIndex(i) + paHost := C.Pa_GetHostApiInfo(i) + devs := make([]*DeviceInfo, paHost.deviceCount) + for j := range devs { + devs[j] = devices[C.Pa_HostApiDeviceIndexToDeviceIndex(i, C.int(j))] + } + hostApis[i] = &HostApiInfo{ + Type: HostApiType(paHost._type), + Name: C.GoString(paHost.name), + DefaultInputDevice: lookupDevice(devices, paHost.defaultInputDevice), + DefaultOutputDevice: lookupDevice(devices, paHost.defaultOutputDevice), + Devices: devs, + } + } + for i := range devices { + devices[i].HostApi = hostApis[hosti[i]] + } + cached = true + } + return hostApis, devices, nil +} + +func duration(paTime C.PaTime) time.Duration { + return time.Duration(paTime * C.PaTime(time.Second)) +} + +func lookupDevice(d []*DeviceInfo, i C.PaDeviceIndex) *DeviceInfo { + if i >= 0 { + return d[i] + } + return nil +} + +// StreamParameters includes all parameters required to +// open a stream except for the callback or buffers. +type StreamParameters struct { + Input, Output StreamDeviceParameters + SampleRate float64 + FramesPerBuffer int + Flags StreamFlags +} + +// StreamDeviceParameters specifies parameters for +// one device (either input or output) in a stream. +// A nil Device indicates that no device is to be used +// -- i.e., for an input- or output-only stream. +type StreamDeviceParameters struct { + Device *DeviceInfo + Channels int + Latency time.Duration +} + +// FramesPerBufferUnspecified ... +const FramesPerBufferUnspecified = C.paFramesPerBufferUnspecified + +// StreamFlags ... +type StreamFlags C.PaStreamFlags + +const ( + NoFlag StreamFlags = C.paNoFlag + ClipOff StreamFlags = C.paClipOff + DitherOff StreamFlags = C.paDitherOff + NeverDropInput StreamFlags = C.paNeverDropInput + PrimeOutputBuffersUsingStreamCallback StreamFlags = C.paPrimeOutputBuffersUsingStreamCallback + PlatformSpecificFlags StreamFlags = C.paPlatformSpecificFlags +) + +// HighLatencyParameters are mono in, stereo out (if supported), +// high latency, the smaller of the default sample rates of the two devices, +// and FramesPerBufferUnspecified. One of the devices may be nil. +func HighLatencyParameters(in, out *DeviceInfo) (p StreamParameters) { + sampleRate := 0.0 + if in != nil { + p := &p.Input + p.Device = in + p.Channels = 1 + if in.MaxInputChannels < 1 { + p.Channels = in.MaxInputChannels + } + p.Latency = in.DefaultHighInputLatency + sampleRate = in.DefaultSampleRate + } + if out != nil { + p := &p.Output + p.Device = out + p.Channels = 2 + if out.MaxOutputChannels < 2 { + p.Channels = out.MaxOutputChannels + } + p.Latency = out.DefaultHighOutputLatency + if r := out.DefaultSampleRate; r < sampleRate || sampleRate == 0 { + sampleRate = r + } + } + p.SampleRate = sampleRate + p.FramesPerBuffer = FramesPerBufferUnspecified + return p +} + +// LowLatencyParameters are mono in, stereo out (if supported), +// low latency, the larger of the default sample rates of the two devices, +// and FramesPerBufferUnspecified. One of the devices may be nil. +func LowLatencyParameters(in, out *DeviceInfo) (p StreamParameters) { + sampleRate := 0.0 + if in != nil { + p := &p.Input + p.Device = in + p.Channels = 1 + if in.MaxInputChannels < 1 { + p.Channels = in.MaxInputChannels + } + p.Latency = in.DefaultLowInputLatency + sampleRate = in.DefaultSampleRate + } + if out != nil { + p := &p.Output + p.Device = out + p.Channels = 2 + if out.MaxOutputChannels < 2 { + p.Channels = out.MaxOutputChannels + } + p.Latency = out.DefaultLowOutputLatency + if r := out.DefaultSampleRate; r > sampleRate { + sampleRate = r + } + } + p.SampleRate = sampleRate + p.FramesPerBuffer = FramesPerBufferUnspecified + return p +} + +// IsFormatSupported Returns nil if the format is supported, otherwise an error. +// The args parameter has the same meaning as in OpenStream. +func IsFormatSupported(p StreamParameters, args ...interface{}) error { + s := &Stream{} + err := s.init(p, args...) + if err != nil { + return err + } + return newError(C.Pa_IsFormatSupported(s.inParams, s.outParams, C.double(p.SampleRate))) +} + +// Int24 ... +type Int24 [3]byte + +// Stream provides access to audio hardware represented +// by one or more PaDevices. Depending on the underlying +// Host API, it may be possible to open multiple streams +// using the same device, however this behavior is +// implementation defined. +// +// Portable applications should assume that a Device may be simultaneously used by at most one Stream. +type Stream struct { + id uintptr + paStream unsafe.Pointer + inParams, outParams *C.PaStreamParameters + in, out *reflect.SliceHeader + timeInfo StreamCallbackTimeInfo + flags StreamCallbackFlags + args []reflect.Value + callback reflect.Value + closed bool +} + +/* +Since Go 1.6, if a Go pointer is passed to C then the Go memory it points to +may not contain any Go pointers: https://golang.org/cmd/cgo/#hdr-Passing_pointers +To deal with this, we maintain an id-keyed map of active streams. +*/ +var ( + mu sync.RWMutex + streams = map[uintptr]*Stream{} + nextID uintptr +) + +func newStream() *Stream { + mu.Lock() + defer mu.Unlock() + s := &Stream{id: nextID} + streams[nextID] = s + nextID++ + return s +} + +func getStream(id uintptr) *Stream { + mu.RLock() + defer mu.RUnlock() + return streams[id] +} + +func delStream(s *Stream) { + mu.Lock() + defer mu.Unlock() + delete(streams, s.id) +} + +/* +StreamCallback exists for documentation purposes only. + +A StreamCallback is a func whose signature resembles + + func(in Buffer, out Buffer, timeInfo StreamCallbackTimeInfo, flags StreamCallbackFlags) + +where the final one or two parameters may be omitted. For an input- or output-only stream, one of the Buffer parameters may also be omitted. The two Buffer types may be different. +*/ +type StreamCallback interface{} + +/* +Buffer exists for documentation purposes only. + +A Buffer is of the form [][]SampleType or []SampleType +where SampleType is float32, int32, Int24, int16, int8, or uint8. + +In the first form, channels are non-interleaved: +len(buf) == numChannels and len(buf[i]) == framesPerBuffer + +In the second form, channels are interleaved: +len(buf) == numChannels * framesPerBuffer +*/ +type Buffer interface{} + +// StreamCallbackTimeInfo contains timing information for the +// buffers passed to the stream callback. +type StreamCallbackTimeInfo struct { + InputBufferAdcTime, CurrentTime, OutputBufferDacTime time.Duration +} + +// StreamCallbackFlags are flag bit constants for the statusFlags to StreamCallback. +type StreamCallbackFlags C.PaStreamCallbackFlags + +// PortAudio stream callback flags. +const ( + // In a stream opened with FramesPerBufferUnspecified, + // InputUnderflow indicates that input data is all silence (zeros) + // because no real data is available. + // + // In a stream opened without FramesPerBufferUnspecified, + // InputUnderflow indicates that one or more zero samples have been inserted + // into the input buffer to compensate for an input underflow. + InputUnderflow StreamCallbackFlags = C.paInputUnderflow + + // In a stream opened with FramesPerBufferUnspecified, + // indicates that data prior to the first sample of the + // input buffer was discarded due to an overflow, possibly + // because the stream callback is using too much CPU time. + // + // Otherwise indicates that data prior to one or more samples + // in the input buffer was discarded. + InputOverflow StreamCallbackFlags = C.paInputOverflow + + // Indicates that output data (or a gap) was inserted, + // possibly because the stream callback is using too much CPU time. + OutputUnderflow StreamCallbackFlags = C.paOutputUnderflow + + // Indicates that output data will be discarded because no room is available. + OutputOverflow StreamCallbackFlags = C.paOutputOverflow + + // Some of all of the output data will be used to prime the stream, + // input data may be zero. + PrimingOutput StreamCallbackFlags = C.paPrimingOutput +) + +// OpenStream creates an instance of a Stream. +// +// For an input- or output-only stream, p.Output.Device or p.Input.Device must be nil, respectively. +// +// The args may consist of either a single StreamCallback or, +// for a blocking stream, two Buffers or pointers to Buffers. +// +// For an input- or output-only stream, one of the Buffer args may be omitted. +func OpenStream(p StreamParameters, args ...interface{}) (*Stream, error) { + if initialized <= 0 { + return nil, NotInitialized + } + + s := newStream() + err := s.init(p, args...) + if err != nil { + delStream(s) + return nil, err + } + cb := C.paStreamCallback + if !s.callback.IsValid() { + cb = nil + } + paErr := C.Pa_OpenStream(&s.paStream, s.inParams, s.outParams, C.double(p.SampleRate), C.ulong(p.FramesPerBuffer), C.PaStreamFlags(p.Flags), cb, unsafe.Pointer(s.id)) + if paErr != C.paNoError { + delStream(s) + return nil, newError(paErr) + } + return s, nil +} + +// OpenDefaultStream is a simplified version of OpenStream that +// opens the default input and/or output devices. +// +// The args parameter has the same meaning as in OpenStream. +func OpenDefaultStream(numInputChannels, numOutputChannels int, sampleRate float64, framesPerBuffer int, args ...interface{}) (*Stream, error) { + if initialized <= 0 { + return nil, NotInitialized + } + + var inDev, outDev *DeviceInfo + var err error + if numInputChannels > 0 { + inDev, err = DefaultInputDevice() + if err != nil { + return nil, err + } + } + if numOutputChannels > 0 { + outDev, err = DefaultOutputDevice() + if err != nil { + return nil, err + } + } + p := HighLatencyParameters(inDev, outDev) + p.Input.Channels = numInputChannels + p.Output.Channels = numOutputChannels + p.SampleRate = sampleRate + p.FramesPerBuffer = framesPerBuffer + return OpenStream(p, args...) +} + +func (s *Stream) init(p StreamParameters, args ...interface{}) error { + switch len(args) { + case 0: + return fmt.Errorf("too few args") + case 1, 2: + if fun := reflect.ValueOf(args[0]); fun.Kind() == reflect.Func { + return s.initCallback(p, fun) + } + return s.initBuffers(p, args...) + default: + return fmt.Errorf("too many args") + } +} + +func (s *Stream) initCallback(p StreamParameters, fun reflect.Value) error { + t := fun.Type() + if t.IsVariadic() { + return fmt.Errorf("StreamCallback must not be variadic") + } + nArgs := t.NumIn() + if nArgs == 0 { + return fmt.Errorf("too few parameters in StreamCallback") + } + args := make([]reflect.Value, nArgs) + i := 0 + bothBufs := nArgs > 1 && t.In(1).Kind() == reflect.Slice + bufArg := func(p StreamDeviceParameters) (*C.PaStreamParameters, *reflect.SliceHeader, error) { + if p.Device != nil || bothBufs { + if i >= nArgs { + return nil, nil, fmt.Errorf("too few Buffer parameters in StreamCallback") + } + t := t.In(i) + sampleFmt := sampleFormat(t) + if sampleFmt == 0 { + return nil, nil, fmt.Errorf("expected Buffer type in StreamCallback, got %v", t) + } + buf := reflect.New(t) + args[i] = buf.Elem() + i++ + if p.Device != nil { + pap := paStreamParameters(p, sampleFmt) + if pap.sampleFormat&C.paNonInterleaved != 0 { + n := int(pap.channelCount) + buf.Elem().Set(reflect.MakeSlice(t, n, n)) + } + return pap, (*reflect.SliceHeader)(unsafe.Pointer(buf.Pointer())), nil + } + } + return nil, nil, nil + } + var err error + s.inParams, s.in, err = bufArg(p.Input) + if err != nil { + return err + } + s.outParams, s.out, err = bufArg(p.Output) + if err != nil { + return err + } + if i < nArgs { + t := t.In(i) + if t != reflect.TypeOf(StreamCallbackTimeInfo{}) { + return fmt.Errorf("invalid StreamCallback") + } + args[i] = reflect.ValueOf(&s.timeInfo).Elem() + i++ + } + if i < nArgs { + t := t.In(i) + if t != reflect.TypeOf(StreamCallbackFlags(0)) { + return fmt.Errorf("invalid StreamCallback") + } + args[i] = reflect.ValueOf(&s.flags).Elem() + i++ + } + if i < nArgs { + return fmt.Errorf("too many parameters in StreamCallback") + } + if t.NumOut() > 0 { + return fmt.Errorf("too many results in StreamCallback") + } + s.callback = fun + s.args = args + return nil +} + +func (s *Stream) initBuffers(p StreamParameters, args ...interface{}) error { + bothBufs := len(args) == 2 + bufArg := func(p StreamDeviceParameters) (*C.PaStreamParameters, *reflect.SliceHeader, error) { + if p.Device != nil || bothBufs { + if len(args) == 0 { + return nil, nil, fmt.Errorf("too few Buffer args") + } + arg := reflect.ValueOf(args[0]) + args = args[1:] + t := arg.Type() + if t.Kind() == reflect.Ptr { + t = t.Elem() + } else { + argPtr := reflect.New(t) + argPtr.Elem().Set(arg) + arg = argPtr + } + sampleFmt := sampleFormat(t) + if sampleFmt == 0 { + return nil, nil, fmt.Errorf("invalid Buffer type %v", t) + } + if arg.IsNil() { + return nil, nil, fmt.Errorf("nil Buffer pointer") + } + if p.Device != nil { + return paStreamParameters(p, sampleFmt), (*reflect.SliceHeader)(unsafe.Pointer(arg.Pointer())), nil + } + } + return nil, nil, nil + } + var err error + s.inParams, s.in, err = bufArg(p.Input) + if err != nil { + return err + } + s.outParams, s.out, err = bufArg(p.Output) + if err != nil { + return err + } + return nil +} + +func sampleFormat(b reflect.Type) (f C.PaSampleFormat) { + if b.Kind() != reflect.Slice { + return 0 + } + b = b.Elem() + if b.Kind() == reflect.Slice { + f = C.paNonInterleaved + b = b.Elem() + } + switch b.Kind() { + case reflect.Float32: + f |= C.paFloat32 + case reflect.Int32: + f |= C.paInt32 + default: + if b == reflect.TypeOf(Int24{}) { + f |= C.paInt24 + } else { + return 0 + } + case reflect.Int16: + f |= C.paInt16 + case reflect.Int8: + f |= C.paInt8 + case reflect.Uint8: + f |= C.paUInt8 + } + return f +} + +func paStreamParameters(p StreamDeviceParameters, fmt C.PaSampleFormat) *C.PaStreamParameters { + return &C.PaStreamParameters{ + device: p.Device.index, + channelCount: C.int(p.Channels), + sampleFormat: fmt, + suggestedLatency: C.PaTime(p.Latency.Seconds()), + } +} + +// Close terminates the stream. +func (s *Stream) Close() error { + if !s.closed { + s.closed = true + err := newError(C.Pa_CloseStream(s.paStream)) + delStream(s) + return err + } + return nil +} + +// Start commences audio processing. +func (s *Stream) Start() error { + return newError(C.Pa_StartStream(s.paStream)) +} + +//export streamCallback +func streamCallback(inputBuffer, outputBuffer unsafe.Pointer, frames C.ulong, timeInfo *C.PaStreamCallbackTimeInfo, statusFlags C.PaStreamCallbackFlags, userData unsafe.Pointer) { + defer func() { + // Don't let PortAudio silently swallow panics. + if x := recover(); x != nil { + buf := make([]byte, 1<<10) + for runtime.Stack(buf, true) == len(buf) { + buf = make([]byte, 2*len(buf)) + } + fmt.Fprintf(os.Stderr, "panic in portaudio stream callback: %s\n\n%s", x, buf) + os.Exit(2) + } + }() + + s := getStream(uintptr(userData)) + s.timeInfo = StreamCallbackTimeInfo{duration(timeInfo.inputBufferAdcTime), duration(timeInfo.currentTime), duration(timeInfo.outputBufferDacTime)} + s.flags = StreamCallbackFlags(statusFlags) + updateBuffer(s.in, uintptr(inputBuffer), s.inParams, int(frames)) + updateBuffer(s.out, uintptr(outputBuffer), s.outParams, int(frames)) + s.callback.Call(s.args) +} + +func updateBuffer(buf *reflect.SliceHeader, p uintptr, params *C.PaStreamParameters, frames int) { + if p == 0 { + return + } + if params.sampleFormat&C.paNonInterleaved == 0 { + setSlice(buf, p, frames*int(params.channelCount)) + } else { + setChannels(buf, p, frames) + } +} + +func setChannels(s *reflect.SliceHeader, p uintptr, frames int) { + sp := s.Data + for i := 0; i < s.Len; i++ { + setSlice((*reflect.SliceHeader)(unsafe.Pointer(sp)), *(*uintptr)(unsafe.Pointer(p)), frames) + sp += unsafe.Sizeof(reflect.SliceHeader{}) + p += unsafe.Sizeof(uintptr(0)) + } +} + +func setSlice(s *reflect.SliceHeader, data uintptr, n int) { + s.Data = data + s.Len = n + s.Cap = n +} + +// Stop terminates audio processing. It waits until all pending +// audio buffers have been played before it returns. +func (s *Stream) Stop() error { + return newError(C.Pa_StopStream(s.paStream)) +} + +// Abort terminates audio processing immediately +// without waiting for pending buffers to complete. +func (s *Stream) Abort() error { + return newError(C.Pa_AbortStream(s.paStream)) +} + +// Info returns information about the Stream instance. +func (s *Stream) Info() *StreamInfo { + i := C.Pa_GetStreamInfo(s.paStream) + if i == nil { + return nil + } + return &StreamInfo{duration(i.inputLatency), duration(i.outputLatency), float64(i.sampleRate)} +} + +// StreamInfo contains information about the stream. +type StreamInfo struct { + InputLatency, OutputLatency time.Duration + SampleRate float64 +} + +// Time returns the current time in seconds for a lifespan of a stream. +// Starting and stopping the stream does not affect the passage of time. +func (s *Stream) Time() time.Duration { + return duration(C.Pa_GetStreamTime(s.paStream)) +} + +// CpuLoad returns the CPU usage information for the specified stream, +// where 0.0 is 0% usage and 1.0 is 100% usage. +// +// The "CPU Load" is a fraction of total CPU time consumed by a +// callback stream's audio processing routines including, +// but not limited to the client supplied stream callback. +// +// This function does not work with blocking read/write streams. +// +// This function may be called from the stream callback function or the application. +func (s *Stream) CpuLoad() float64 { + return float64(C.Pa_GetStreamCpuLoad(s.paStream)) +} + +// AvailableToRead returns the number of frames that +// can be read from the stream without waiting. +func (s *Stream) AvailableToRead() (int, error) { + n := C.Pa_GetStreamReadAvailable(s.paStream) + if n < 0 { + return 0, newError(C.PaError(n)) + } + return int(n), nil +} + +// AvailableToWrite returns the number of frames that +// can be written from the stream without waiting. +func (s *Stream) AvailableToWrite() (int, error) { + n := C.Pa_GetStreamWriteAvailable(s.paStream) + if n < 0 { + return 0, newError(C.PaError(n)) + } + return int(n), nil +} + +// Read uses the buffer provided to OpenStream. +// The number of samples to read is determined by the size of the buffer. +func (s *Stream) Read() error { + if s.callback.IsValid() { + return CanNotReadFromACallbackStream + } + if s.in == nil { + return CanNotReadFromAnOutputOnlyStream + } + buf, frames, err := getBuffer(s.in, s.inParams) + if err != nil { + return err + } + return newError(C.Pa_ReadStream(s.paStream, buf, C.ulong(frames))) +} + +// Write uses the buffer provided to OpenStream. +// The number of samples to write is determined by the size of the buffer. +func (s *Stream) Write() error { + if s.callback.IsValid() { + return CanNotWriteToACallbackStream + } + if s.out == nil { + return CanNotWriteToAnInputOnlyStream + } + buf, frames, err := getBuffer(s.out, s.outParams) + if err != nil { + return err + } + return newError(C.Pa_WriteStream(s.paStream, buf, C.ulong(frames))) +} + +func getBuffer(s *reflect.SliceHeader, p *C.PaStreamParameters) (unsafe.Pointer, int, error) { + if p.sampleFormat&C.paNonInterleaved == 0 { + n := int(p.channelCount) + if s.Len%n != 0 { + return nil, 0, fmt.Errorf("length of interleaved buffer not divisible by number of channels") + } + return unsafe.Pointer(s.Data), s.Len / n, nil + } else { + if s.Len != int(p.channelCount) { + return nil, 0, fmt.Errorf("buffer has wrong number of channels") + } + buf := make([]uintptr, s.Len) + frames := -1 + sp := s.Data + for i := range buf { + ch := (*reflect.SliceHeader)(unsafe.Pointer(sp)) + if frames == -1 { + frames = ch.Len + } else if ch.Len != frames { + return nil, 0, fmt.Errorf("channels have different lengths") + } + buf[i] = ch.Data + sp += unsafe.Sizeof(reflect.SliceHeader{}) + } + return unsafe.Pointer(&buf[0]), frames, nil + } +} gemini://vigrey.com/git/modem-tap/commit/91f6bf7191523ec4cca658314173f7d806dedb6c.txt

-- Leo's gemini proxy

-- Connecting to vigrey.com:1965...

-- Connected

-- Sending request

-- Meta line: 20 text/plain; charset=UTF-8

-- Response ended

-- Page fetched on Mon Jun 3 00:13:39 2024