Created
July 14, 2017 00:19
-
-
Save moutend/f53beedd6b69c329a6b28f79cb07119b 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
package main | |
import ( | |
"context" | |
"flag" | |
"fmt" | |
"io/ioutil" | |
"log" | |
"os" | |
"os/signal" | |
"strings" | |
"time" | |
"unsafe" | |
"github.com/go-ole/go-ole" | |
"github.com/moutend/go-wav" | |
"github.com/moutend/go-wca" | |
"github.com/moutend/go-hook/keyboard" | |
) | |
var version = "latest" | |
var revision = "latest" | |
var offset int | |
var tx = time.Now() | |
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) | |
} | |
} | |
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 | |
} | |
ctx, cancel := context.WithCancel(context.Background()) | |
signalChan := make(chan os.Signal, 1) | |
signal.Notify(signalChan, os.Interrupt) | |
keyboardChan := make(chan keyboard.KBDLLHOOKSTRUCT, 1) | |
go keyboard.Notify(ctx, keyboardChan) | |
fmt.Println("started") | |
var state bool | |
go func() { | |
err = renderSharedTimerDriven(ctx, audio) | |
fmt.Println(err) | |
}() | |
for { | |
if state { | |
break | |
} | |
select { | |
case <-signalChan: | |
fmt.Println("Interrupted by SIGINT") | |
state = true | |
cancel() | |
case v := <-keyboardChan: | |
//fmt.Println(v) | |
if v.Flags == 0 { | |
tx = time.Now() | |
offset = 0 | |
} | |
} | |
} | |
fmt.Println("Successfully done") | |
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 | |
} | |
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 | |
} | |
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, 10*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 input = audio.Bytes() | |
var data *byte | |
var padding uint32 | |
var availableFrameSize uint32 | |
var b *byte | |
var isPlaying bool = true | |
for { | |
if !isPlaying { | |
break | |
} | |
select { | |
case <-ctx.Done(): | |
isPlaying = false | |
break | |
default: | |
if offset >= audio.Length() { | |
offset = -1 | |
} | |
if offset == 0 { | |
fmt.Println(time.Now().Sub(tx)) | |
} | |
if err = ac.GetCurrentPadding(&padding); err != nil { | |
return | |
} | |
availableFrameSize = bufferFrameSize - padding | |
if availableFrameSize == 0 { | |
continue | |
} | |
if err = arc.GetBuffer(availableFrameSize, &data); err != nil { | |
return | |
} | |
if offset > -1 { | |
start := unsafe.Pointer(data) | |
lim := int(availableFrameSize) * int(wfx.NBlockAlign) | |
remaining := audio.Length() - offset | |
if remaining < lim { | |
lim = remaining | |
} | |
for n := 0; n < lim; n++ { | |
b = (*byte)(unsafe.Pointer(uintptr(start) + uintptr(n))) | |
*b = input[offset+n] | |
} | |
offset += lim | |
} else { | |
start := unsafe.Pointer(data) | |
lim := int(availableFrameSize) * int(wfx.NBlockAlign) | |
for n := 0; n < lim; n++ { | |
b = (*byte)(unsafe.Pointer(uintptr(start) + uintptr(n))) | |
*b = 0 | |
} | |
} | |
if err = arc.ReleaseBuffer(availableFrameSize, 0); err != nil { | |
return | |
} | |
//@@@ | |
time.Sleep(renderingPeriod) | |
} | |
} | |
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