Created
July 14, 2017 10:03
-
-
Save moutend/a9e016b93f498c32504499a5523e28ef to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// +build windows | |
package main | |
import ( | |
"bytes" | |
"context" | |
"encoding/binary" | |
"flag" | |
"fmt" | |
"io/ioutil" | |
"log" | |
"math" | |
"os" | |
"os/signal" | |
"strings" | |
"sync" | |
"time" | |
"unsafe" | |
"github.com/go-ole/go-ole" | |
"github.com/moutend/go-wav" | |
"github.com/moutend/go-wca" | |
) | |
var version = "latest" | |
var revision = "latest" | |
var wg sync.WaitGroup | |
var inputChan chan float64 | |
var latency uint32 = 200 // millisecond | |
type FilenameFlag struct { | |
Value string | |
} | |
func (f *FilenameFlag) Set(value string) (err error) { | |
if !strings.HasSuffix(value, ".wav") { | |
err = fmt.Errorf("specify WAVE audio file (*.wav)") | |
return | |
} | |
f.Value = value | |
return | |
} | |
func (f *FilenameFlag) String() string { | |
return f.Value | |
} | |
func main() { | |
var err error | |
if err = run(os.Args); err != nil { | |
log.Fatal(err) | |
} | |
fmt.Println("Successfully done") | |
} | |
func run(args []string) (err error) { | |
var filenameFlag FilenameFlag | |
var versionFlag bool | |
var audio = &wav.File{} | |
var file []byte | |
f := flag.NewFlagSet(args[0], flag.ExitOnError) | |
f.Var(&filenameFlag, "input", "Specify WAVE format audio (e.g. music.wav)") | |
f.Var(&filenameFlag, "i", "Alias of --input") | |
f.BoolVar(&versionFlag, "version", false, "Show version") | |
f.Parse(args[1:]) | |
if versionFlag { | |
fmt.Printf("%s-%s\n", version, revision) | |
return | |
} | |
if filenameFlag.Value == "" { | |
return | |
} | |
if file, err = ioutil.ReadFile(filenameFlag.Value); err != nil { | |
return | |
} | |
if err = wav.Unmarshal(file, audio); err != nil { | |
return | |
} | |
errChan := make(chan error, 1) | |
signalChan := make(chan os.Signal, 1) | |
signal.Notify(signalChan, os.Interrupt) | |
ctx, cancel := context.WithCancel(context.Background()) | |
fs := audio.Float64s() | |
fmt.Println("fs:", len(fs)) | |
inputChan = make(chan float64, len(fs)) | |
for _, v := range fs { | |
inputChan <- v | |
} | |
close(inputChan) | |
wg.Add(1) | |
go func() { | |
if err = renderSharedTimerDriven(ctx, audio); err != nil { | |
errChan <- err | |
} | |
wg.Done() | |
}() | |
go func() { | |
select { | |
case err = <-errChan: | |
case <-signalChan: | |
fmt.Println("Interrupted by SIGINT") | |
cancel() | |
} | |
}() | |
wg.Wait() | |
return | |
} | |
func renderSharedTimerDriven(ctx context.Context, audio *wav.File) (err error) { | |
if err = ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED); err != nil { | |
return | |
} | |
defer ole.CoUninitialize() | |
var de *wca.IMMDeviceEnumerator | |
if err = wca.CoCreateInstance(wca.CLSID_MMDeviceEnumerator, 0, wca.CLSCTX_ALL, wca.IID_IMMDeviceEnumerator, &de); err != nil { | |
return | |
} | |
defer de.Release() | |
var mmd *wca.IMMDevice | |
if err = de.GetDefaultAudioEndpoint(wca.ERender, wca.EConsole, &mmd); err != nil { | |
return | |
} | |
defer mmd.Release() | |
var ps *wca.IPropertyStore | |
if err = mmd.OpenPropertyStore(wca.STGM_READ, &ps); err != nil { | |
return | |
} | |
defer ps.Release() | |
var pv wca.PROPVARIANT | |
if err = ps.GetValue(&wca.PKEY_Device_FriendlyName, &pv); err != nil { | |
return | |
} | |
fmt.Printf("Rendering audio to: %s\n", pv.String()) | |
var ac *wca.IAudioClient | |
if err = mmd.Activate(wca.IID_IAudioClient, wca.CLSCTX_ALL, nil, &ac); err != nil { | |
return | |
} | |
defer ac.Release() | |
var wfx *wca.WAVEFORMATEX | |
if err = ac.GetMixFormat(&wfx); err != nil { | |
return | |
} | |
defer ole.CoTaskMemFree(uintptr(unsafe.Pointer(wfx))) | |
if wfx.WFormatTag != wca.WAVE_FORMAT_PCM { | |
wfx.WFormatTag = 1 | |
wfx.NSamplesPerSec = uint32(audio.SamplesPerSec()) | |
wfx.WBitsPerSample = uint16(audio.BitsPerSample()) | |
wfx.NChannels = uint16(audio.Channels()) | |
wfx.NBlockAlign = uint16(audio.BlockAlign()) | |
wfx.NAvgBytesPerSec = uint32(audio.AvgBytesPerSec()) | |
wfx.CbSize = 0 | |
} | |
fmt.Println("--------") | |
fmt.Printf("Format: PCM %d bit signed integer\n", wfx.WBitsPerSample) | |
fmt.Printf("Rate: %d Hz\n", wfx.NSamplesPerSec) | |
fmt.Printf("Channels: %d\n", wfx.NChannels) | |
fmt.Println("--------") | |
var defaultPeriod int64 | |
var minimumPeriod int64 | |
var renderingPeriod time.Duration | |
if err = ac.GetDevicePeriod(&defaultPeriod, &minimumPeriod); err != nil { | |
return | |
} | |
renderingPeriod = time.Duration(int(defaultPeriod) * 100) | |
fmt.Printf("Default rendering period: %d ms\n", renderingPeriod/time.Millisecond) | |
if err = ac.Initialize(wca.AUDCLNT_SHAREMODE_SHARED, 0, latency*10000, 0, wfx, nil); err != nil { | |
return | |
} | |
var bufferFrameSize uint32 | |
if err = ac.GetBufferSize(&bufferFrameSize); err != nil { | |
return | |
} | |
fmt.Printf("Allocated buffer size: %d\n", bufferFrameSize) | |
var arc *wca.IAudioRenderClient | |
if err = ac.GetService(wca.IID_IAudioRenderClient, &arc); err != nil { | |
return | |
} | |
defer arc.Release() | |
if err = ac.Start(); err != nil { | |
return | |
} | |
fmt.Println("Start rendering audio with shared-timer-driven mode") | |
fmt.Println("Press Ctrl-C to stop rendering") | |
time.Sleep(renderingPeriod) | |
var data *byte | |
var padding uint32 | |
var availableFrameSize uint32 | |
var b *byte | |
var isPlaying bool = true | |
scale := math.Pow(2.0, float64(wfx.WBitsPerSample)-1.0) | |
fmt.Println("scale", scale) | |
for { | |
if !isPlaying { | |
break | |
} | |
select { | |
case <-ctx.Done(): | |
isPlaying = false | |
break | |
default: | |
if err = ac.GetCurrentPadding(&padding); err != nil { | |
return | |
} | |
availableFrameSize = bufferFrameSize - padding | |
if availableFrameSize == 0 { | |
continue | |
} | |
if err = arc.GetBuffer(availableFrameSize, &data); err != nil { | |
return | |
} | |
start := unsafe.Pointer(data) | |
lim := int(availableFrameSize) * int(wfx.NBlockAlign) | |
samples := int(availableFrameSize) * int(wfx.NChannels) | |
ds := make([]byte, lim) | |
var f float64 | |
for i := 0; i < samples; i++ { | |
f, isPlaying = <-inputChan | |
v := int16(scale * f) | |
buf := new(bytes.Buffer) | |
binary.Write(buf, binary.LittleEndian, v) | |
bs := buf.Bytes() | |
ds[i*2] = bs[0] | |
ds[i*2+1] = bs[1] | |
} | |
for n := 0; n < lim; n++ { | |
b = (*byte)(unsafe.Pointer(uintptr(start) + uintptr(n))) | |
*b = ds[n] | |
} | |
if err = arc.ReleaseBuffer(availableFrameSize, 0); err != nil { | |
return | |
} | |
time.Sleep(renderingPeriod) | |
} | |
} | |
time.Sleep(time.Duration(latency) * time.Millisecond) | |
if err = ac.Stop(); err != nil { | |
return | |
} | |
fmt.Println("Stop rendering loopback audio") | |
return | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment