Repository: bluefireteam/audioplayers

Issue #489: windows platform support
Created: 2020-04-07T10:48:17Z
flutter desktop is not stable, but we all know it’s future of desktop app development.
audioplayer works fine on android ios web and macos but only windows does not have support for it. thous it is experimental i think we should support windows platform in early stage

Duplicate issue #944: Add Windows platform support (based on bass.dll)
Created: 2021-07-06T03:05:49Z
Thanks
Thank you very much for having audiolayers. I used it very early and helped me a lot. Thank you.
Add Windows Platform support
On Windows platform, you can use bass.dll to play audio. This is a very simple and lightweight dynamic link library (official website: http://www.un4seen.com ）
The following code is a simple encapsulation of bass.dll by dart:
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:ffi';
import 'package:audioplayers/audioplayers.dart';
import 'package:ffi/ffi.dart' as ffi;
import 'package:flutter/services.dart';

typedef _BassInitN = Int32 Function(Int32 device, Uint32 freq, Uint32 flags, Uint32 win, Pointer clsid);
typedef _BassInitD = int Function(int device, int freq, int flags, int win, Pointer clsid);

typedef _BassStreamCreateN = Uint32 Function(Uint32 freq, Uint32 chans, Uint32 flags, Pointer proc, Pointer user);
typedef _BassStreamCreateD = int Function(int freq, int chans, int flags, Pointer proc, Pointer user);

typedef _BassStreamCreateUrlN = Uint32 Function(Pointer<ffi.Utf8> url, Uint32 offset, Uint32 flags, Pointer proc, Pointer user);
typedef _BassStreamCreateUrlD = int Function(Pointer<ffi.Utf8> url, int offset, int flags, Pointer proc, Pointer user);

// by yangyxd 2020.07.16
// homePage: https://github.com/yangyxd
class BassWinUtil {
  static DynamicLibrary _library;
  static int floatable = 0;
  static bool isInit = false;

  static void windowsInit() {
    if (_library != null) return;
    final dll = "bass.dll";
    try {
      _library = DynamicLibrary.open(dll);
    } catch (e) {
      stderr.writeln('Failed to load $dll');
      rethrow;
    }
  }

  static void linuxInit() {
    if (_library != null) return;
    final so = "libbass.so";
    try {
      _library = DynamicLibrary.open(so);
    } catch (e) {
      stderr.writeln('Failed to load $so');
      rethrow;
    }
  }

  static int BASS_SAMPLE_FLOAT = 256;
  static int BASS_SAMPLE_LOOP = 4;
  static int BASS_MUSIC_LOOP = BASS_SAMPLE_LOOP;
  static int BASS_MUSIC_RAMPS = 0x400;
  static int BASS_UNICODE = 0x80000000;
  static int BASS_POS_BYTE = 0;
  static int BASS_ACTIVE_STOPPED = 0;
  static int BASS_ACTIVE_PLAYING = 1;
  static int BASS_ACTIVE_STALLED = 2;
  static int BASS_ACTIVE_PAUSED = 3;
  static int BASS_ACTIVE_PAUSED_DEVICE = 4;
  static Pointer<void> nil = Pointer.fromAddress(0);

  static int lastId;
  static ReleaseMode releaseMode;
  static Future<dynamic> Function(MethodCall call) handler;
  static Timer timer;
  /// 0 or null 停止, 1 播放中, 2 暂停
  static int lastState, newState;
  static String curPlayerId;
  static final Map<String, int> player = Map<String, int>();

  static void setMethodCallHandler(String playerId, Future<dynamic> handler(MethodCall call)) {
    BassWinUtil.handler = handler;
    curPlayerId = playerId;
    player.forEach((key, value) {
      stop(value);
    });
    player.clear();
    player[playerId] = 0;
  }

  static void clearTimer() {
    if (timer != null) {
      timer.cancel();
      timer = null;
    }
  }
  
  static void initTimer() {
    clearTimer();
    timer = Timer.periodic(Duration(milliseconds: 200), (timer) {
      if (lastState != newState) {
        lastState = newState ?? 0;
        if (handler == null) return;
        switch (lastState) {
          case 0:
            break;
          case 1:
            handler(MethodCall("audio.onNotificationPlayerStateChanged", {
              "playerId": curPlayerId,
              "value": true,
            }));
            break;
          case 2:
            handler(MethodCall("audio.onNotificationPlayerStateChanged", {
              "playerId": curPlayerId,
              "value": false,
            }));
            break;
        }
      } else if (lastState == 1) {
        if (player[curPlayerId] != lastId) {
          player[curPlayerId] = lastId;
        }
        if (lastId != null && lastId != 0) {
          final value = getCurrentPosition(lastId);
          handler(MethodCall("audio.onCurrentPosition", {
            "playerId": curPlayerId,
            "value": (value * 1000).toInt(),
          }));
          final _state = isActive(lastId);
          var _newState;
          if (_state == BASS_ACTIVE_STOPPED)
            _newState = 0;
          else if (_state == BASS_ACTIVE_PLAYING)
            _newState = 1;
          else if (_state == BASS_ACTIVE_PAUSED)
            _newState = 2;
          if (_newState == 0) {
            handler(MethodCall("audio.onComplete", {
              "playerId": curPlayerId,
            }));
          }
          if (_newState != newState)
            newState = _newState;
        }
      }
    });
  }

  static int lastErrorCode() {
    final int Function() BASS_ErrorGetCode =
      _library.lookup<NativeFunction<Uint32 Function()>>("BASS_ErrorGetCode").asFunction();
    final code = BASS_ErrorGetCode();
    if (code != 0 && handler != null) {
      String msg = "unknown error";
      switch (code) {
        case 1: msg = 'memory error'; break;
        case 2: msg = "can't open the file"; break;
        case 3: msg = "can't find a free sound driver"; break;
        case 4: msg = "the sample buffer was lost"; break;
        case 5: msg = 'invalid handle'; break;
        case 6: msg = 'unsupported sample format'; break;
        case 7: msg = 'invalid position'; return code; break;
        case 8: msg = 'BASS_Init has not been successfully called'; break;
        case 9: msg = 'BASS_Start has not been successfully called'; break;
        case 14: return 14; // msg = 'already initialized/paused/whatever'; break;
        case 17: msg = 'file does not contain audio'; break;
        case 18: msg = "can't get a free channel"; break;
        case 19: msg = 'an illegal type was specified'; break;
        case 20: msg = 'an illegal parameter was specified'; break;
        case 23: msg = 'illegal device number'; break;
        case 24: msg = 'not playing'; return code; break;
        case 25: msg = 'illegal sample rate'; break;
        case 27: msg = 'the stream is not a file stream'; break;
        case 29: msg = 'no hardware voices available'; break;
        case 40: msg = 'connection timedout'; break;
        case 32: msg = 'no internet connection could be opened'; break;
        case 41: msg = 'unsupported file format'; break;
        case 42: msg = 'unavailable speaker'; break;
        case 43: msg = 'invalid BASS version (used by add-ons)'; break;
        case 44: msg = 'codec is not available/supported'; break;
      }
      handler(MethodCall("audio.onError", {
        "playerId": curPlayerId,
        "value": "$msg($code)",
      }));
    }
    return code;
  }

  static int init({bool isLocal = false, double volume = 1.0, String url}) {
    if (Platform.isWindows) {
      windowsInit();
    } else if (Platform.isLinux) {
      linuxInit();
    }
    clearTimer();
    if (_library == null) return 0;
    final int Function() BASS_GetVersion =
        _library.lookup<NativeFunction<Int32 Function()>>("BASS_GetVersion").asFunction();
    print("bass version: ${BASS_GetVersion()}");
    if (isInit != true) {
      final BASS_Init = _library.lookupFunction<_BassInitN, _BassInitD>("BASS_Init");
      var i = BASS_Init(-1, 44100, 0, 0, Pointer.fromAddress(0));
      isInit = i != 0;
      if (!isInit) {
        if (lastErrorCode() == 14) {
          isInit = true; // 已经初始化过
        } else
          return 0;
      }
      final BASS_StreamCreate = _library.lookupFunction<_BassStreamCreateN, _BassStreamCreateD>("BASS_StreamCreate");
      floatable = BASS_StreamCreate(44100, 2, BASS_SAMPLE_FLOAT, nil, nil);
      if (floatable > 0) {
        final void Function(int) BASS_StreamFree =
          _library.lookup<NativeFunction<Void Function(Uint32)>>("BASS_StreamFree").asFunction();
        BASS_StreamFree(floatable);
        floatable = BASS_SAMPLE_FLOAT;
      }
    }

    if (lastId != null) {
      stop(lastId);
      lastId = null;
    }

    var chan = 0;
    if (isLocal != true) {
      final BASS_StreamCreateURL = _library.lookupFunction<_BassStreamCreateUrlN, _BassStreamCreateUrlD>("BASS_StreamCreateURL");
      final Pointer<ffi.Utf8> charPointer = url.toNativeUtf8(); // ffi.Utf8.toUtf8(url);
      try {
        chan = BASS_StreamCreateURL(charPointer, 0, BASS_MUSIC_RAMPS | floatable, nil, nil);
      } finally {
        ffi.malloc.free(charPointer);
      }
    } else {
      return 0;
    }

    if (chan != 0) {
      final int Function(int, int) BASS_ChannelPlay =
        _library.lookup<NativeFunction<Int32 Function(Uint32, Int8)>>("BASS_ChannelPlay").asFunction();
      BASS_ChannelPlay(chan, 1);
      lastId = chan;
      newState = 1;
      if (handler != null) {
        final value = getDuration(lastId);
        handler(MethodCall("audio.onDuration", {
          "playerId": curPlayerId,
          "value": (value * 1000).toInt(),
        }));
      }
      initTimer();
    } else {
      lastErrorCode();
    }

    return chan;
  }

  static int pause(int id) {
    if (_library == null || id == 0) return 0;
    final int Function(int) BASS_ChannelPause =
      _library.lookup<NativeFunction<Int32 Function(Uint32)>>("BASS_ChannelPause").asFunction();
    if (BASS_ChannelPause(id) == 1)
      newState = 2;
    else
      lastErrorCode();
    return 1;
  }

  static int resume(int id) {
    if (_library == null || id == 0) return 0;
    final int Function(int, int) BASS_ChannelPlay =
      _library.lookup<NativeFunction<Int32 Function(Uint32, Int8)>>("BASS_ChannelPlay").asFunction();
    if (BASS_ChannelPlay(id, 0) == 1)
      newState = 1;
    else
      lastErrorCode();
    return 1;
  }

  static int setPosition(int id, double position) {
    if (_library == null || id == 0) return 0;
    final int Function(int, int, int) BASS_ChannelSetPosition =
      _library.lookup<NativeFunction<Int32 Function(Uint32, Int64, Uint32)>>("BASS_ChannelSetPosition").asFunction();
    final int Function(int, double) BASS_ChannelSeconds2Bytes =
      _library.lookup<NativeFunction<Int64 Function(Uint32, Double)>>("BASS_ChannelSeconds2Bytes").asFunction();
    final v = BASS_ChannelSeconds2Bytes(id, position);
    if (BASS_ChannelSetPosition(id, v, BASS_POS_BYTE) != 1)
      lastErrorCode();
    return 1;
  }

  static int stop(int id) {
    if (_library == null || id == 0 || id == null) return 0;
    final int Function(int) BASS_ChannelStop =
      _library.lookup<NativeFunction<Int32 Function(Uint32)>>("BASS_ChannelStop").asFunction();
    final void Function(int) BASS_MusicFree =
       _library.lookup<NativeFunction<Void Function(Uint32)>>("BASS_MusicFree").asFunction();
    final void Function(int) BASS_StreamFree =
      _library.lookup<NativeFunction<Void Function(Uint32)>>("BASS_StreamFree").asFunction();
    BASS_ChannelStop(id);
    BASS_MusicFree(id);
    BASS_StreamFree(id);
    newState = 0;
    return 1;
  }

  static void free(int id) {
    clearTimer();
    if (_library == null || isInit != true) return;
    stop(id);
    if (releaseMode == ReleaseMode.STOP)
      return;
    final int Function() BASS_Free =
     _library.lookup<NativeFunction<Int64 Function()>>("BASS_Free").asFunction();
    BASS_Free();
    isInit = false;
    newState = null;
  }

  static void setVolume(double volume) {
    if (_library == null || isInit != true) return;
    final int Function(double) BASS_SetVolume =
      _library.lookup<NativeFunction<Int32 Function(Float)>>("BASS_SetVolume").asFunction();
    BASS_SetVolume(volume);
    isInit = false;
  }

  static void setReleaseMode(ReleaseMode releaseMode) {
    BassWinUtil.releaseMode = releaseMode;
  }

  static double getDuration(int id) {
    if (_library == null || isInit != true) return 0;
    final int Function(int, int) BASS_ChannelGetLength =
      _library.lookup<NativeFunction<Int64 Function(Uint32, Uint32)>>("BASS_ChannelGetLength").asFunction();
    final double Function(int, int) BASS_ChannelBytes2Seconds =
      _library.lookup<NativeFunction<Double Function(Uint32, Int64)>>("BASS_ChannelBytes2Seconds").asFunction();
    final v = BASS_ChannelGetLength(id, BASS_POS_BYTE);
    return BASS_ChannelBytes2Seconds(id, v);
  }

  static int isActive(int id) {
    if (_library == null || id == 0 || id == null) return 0;
    final int Function(int) BASS_ChannelIsActive =
      _library.lookup<NativeFunction<Int64 Function(Uint32)>>("BASS_ChannelIsActive").asFunction();
    return BASS_ChannelIsActive(id);
  }

  static double getCurrentPosition(int id) {
    if (_library == null || isInit != true) return 0;
    final int Function(int, int) BASS_ChannelGetPosition =
      _library.lookup<NativeFunction<Int64 Function(Uint32, Uint32)>>("BASS_ChannelGetPosition").asFunction();
    final double Function(int, int) BASS_ChannelBytes2Seconds =
      _library.lookup<NativeFunction<Double Function(Uint32, Int64)>>("BASS_ChannelBytes2Seconds").asFunction();
    final v = BASS_ChannelGetPosition(id, BASS_POS_BYTE);
    return BASS_ChannelBytes2Seconds(id, v);
  }

  static String cStringToString(Pointer<Uint8> str, int len) {
    if (str == null || len <= 0) return null;
    final units = List<int>.filled(len, 0);
    for (int i = 0; i < len; ++i) units[i] = str.elementAt(i).value;
    return Utf8Codec().decode(units);
  }
}
Use examples
Next, we can detect the platform in some places of audiolayers.dart and do different processing:
notificationState
set notificationState(AudioPlayerState state) {
    if (Platform.isWindows || Platform.isLinux) {
    } else {
      _notificationPlayerStateController.add(state);
    }
    _audioPlayerState = state;
  }
create
  /// Creates a new instance and assigns an unique id to it.
  AudioPlayer({this.mode = PlayerMode.MEDIA_PLAYER, this.playerId}) {
    this.mode ??= PlayerMode.MEDIA_PLAYER;
    this.playerId ??= _uuid.v4();
    players[playerId] = this;

    if (Platform.isWindows || Platform.isLinux) {
      BassWinUtil.setMethodCallHandler(playerId, platformCallHandler);
    }
play
 ...
  Future<int> play(
    String url, {
    bool isLocal,
    double volume = 1.0,
    // position must be null by default to be compatible with radio streams
    Duration position,
    bool respectSilence = false,
    bool stayAwake = false,
    bool recordingActive = false,
  }) async {
    isLocal ??= isLocalUrl(url);
    volume ??= 1.0;
    respectSilence ??= false;
    stayAwake ??= false;

    if (Platform.isWindows || Platform.isLinux) {
      chan = BassWinUtil.init(isLocal: isLocal, volume: volume, url: url);
      if (chan != 0) {
        state = AudioPlayerState.PLAYING;
      }
      return chan;
    }
...
pause
  Future<int> pause() async {
    if (Platform.isWindows || Platform.isLinux) {
      BassWinUtil.pause(chan);
      state = AudioPlayerState.PAUSED;
      return 1;
    }
stop
  Future<int> stop() async {
    if (Platform.isWindows || Platform.isLinux) {
      BassWinUtil.stop(chan);
      state = AudioPlayerState.STOPPED;
      chan = 0;
      return 1;
    }
resume
  Future<int> resume() async {
    if (Platform.isWindows || Platform.isLinux) {
      BassWinUtil.resume(chan);
      state = AudioPlayerState.PLAYING;
      return 1;
    }
release
  Future<int> release() async {
    if (Platform.isWindows || Platform.isLinux) {
      BassWinUtil.free(chan);
      state = AudioPlayerState.STOPPED;
      chan = 0;
      return 1;
    }
seek
  /// Moves the cursor to the desired position.
  Future<int> seek(Duration position) {
    _positionController.add(position);

    if (Platform.isWindows || Platform.isLinux) {
      BassWinUtil.setPosition(chan, position.inMilliseconds / 1000);
      return Future.value(1);
    }

    return _invokeMethod('seek', {'position': position.inMilliseconds});
  }
setVolume
  Future<int> setVolume(double volume)  {
    if (Platform.isWindows || Platform.isLinux) {
      BassWinUtil.setVolume(volume);
      return Future.value(1);
    }
    return _invokeMethod('setVolume', {'volume': volume});
  }
setReleaseMocde
  Future<int> setReleaseMode(ReleaseMode releaseMode) {
    if (Platform.isWindows || Platform.isLinux) {
      BassWinUtil.setReleaseMode(releaseMode);
      return Future.value(1);
    }
setPlaybackRate
  Future<int> setPlaybackRate({double playbackRate = 1.0}) {
    if (Platform.isWindows || Platform.isLinux) {
      return Future.value(1);
    }
    return _invokeMethod('setPlaybackRate', {'playbackRate': playbackRate});
  }
setNotification
  Future<dynamic> setNotification(
      {String title,
      String albumTitle,
      String artist,
      String imageUrl,
      Duration forwardSkipInterval = Duration.zero,
      Duration backwardSkipInterval = Duration.zero,
      Duration duration = Duration.zero,
      Duration elapsedTime = Duration.zero,
      bool hasPreviousTrack = false,
      bool hasNextTrack = false}) {
    if (Platform.isWindows || Platform.isLinux) {
      return Future.value(1);
    }
setUrl
  Future<int> setUrl(String url,
      {bool isLocal: false, bool respectSilence = false}) {
    if (Platform.isWindows || Platform.isLinux) {
      return Future.value(1);
    }
getDuration
  Future<int> getDuration() {
    if (Platform.isWindows || Platform.isLinux) {
      return Future.value((BassWinUtil.getDuration(chan) * 1000).toInt());
    }
getCurrentPosition
  Future<int> getCurrentPosition() async {
    if (Platform.isWindows || Platform.isLinux) {
      return Future.value((BassWinUtil.getCurrentPosition(chan) * 1000).toInt());
    }
    return _invokeMethod('getCurrentPosition');
  }
Thank you again. I hope audiolayers can support Windows platform faster. use bass.dll or other better ways.

Closing comment (2021-10-19T09:48:02Z): Also, isn't this issue a duplicate of #489 ? Shouldn't this conversation be moved there?