Incremental work

This commit is contained in:
spaceflint 2020-12-07 19:19:28 +02:00
parent 9d2bb9fc65
commit 803415c988
23 changed files with 1263 additions and 21 deletions

1
.gitignore vendored

@ -2,3 +2,4 @@
.vs/
packages/
/TODO
mybuild.bat

@ -14,6 +14,7 @@
<ItemGroup>
<Reference Include="$(ObjDir)Android.dll" />
<Reference Include="$(FNA_DLL)" />
<Reference Include="System.dll" />
<Compile Include="src\**\*.cs" />
<CustomAdditionalCompileInputs Include="FNA.filter" />
<Filter Include="FNA.filter" />

@ -102,3 +102,11 @@
*.Content.*
*.Storage.*
*.Audio.AudioChannels
*.Audio.SoundState
*.Audio.*Exception
*.Media.MediaQueue
*.Media.MediaState
*.Media.SongCollection

29
BNA/lzxdecoder.license Normal file

@ -0,0 +1,29 @@
LzxDecoder.cs was derived from libmspack
Copyright 2003-2004 Stuart Caie
Copyright 2011 Ali Scissons
The LZX method was created by Jonathan Forbes and Tomi Poutanen, adapted
by Microsoft Corporation.
This source file is Dual licensed; meaning the end-user of this source file
may redistribute/modify it under the LGPL 2.1 or MS-PL licenses.
About
-----
This derived work is recognized by Stuart Caie and is authorized to adapt
any changes made to lzxd.c in his libmspack library and will still retain
this dual licensing scheme. Big thanks to Stuart Caie!
This file is a pure C# port of the lzxd.c file from libmspack, with minor
changes towards the decompression of XNB files. The original decompression
software of LZX encoded data was written by Suart Caie in his
libmspack/cabextract projects, which can be located at
http://http://www.cabextract.org.uk/
GNU Lesser General Public License, Version 2.1
----------------------------------------------
https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
Microsoft Public License
------------------------
http://www.opensource.org/licenses/ms-pl.html

22
BNA/monoxna.license Normal file

@ -0,0 +1,22 @@
MIT License
Copyright 2006 The Mono.Xna Team
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -22,7 +22,6 @@ namespace Microsoft.Xna.Framework.Graphics
}
//
// CreateProgram
//

@ -37,7 +37,8 @@ namespace Microsoft.Xna.Framework.Graphics
if ((state.TargetFramebuffer = id[0]) == 0)
return;
}
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, state.TargetFramebuffer);
GLES20.glBindFramebuffer(GLES30.GL_DRAW_FRAMEBUFFER,
state.TargetFramebuffer);
int attachmentIndex = GLES20.GL_COLOR_ATTACHMENT0;
foreach (var renderTarget in renderTargetsCopy)
@ -48,7 +49,7 @@ namespace Microsoft.Xna.Framework.Graphics
// from FNA3D_GetMaxMultiSampleCount, which we never do
throw new PlatformNotSupportedException();
/*GLES20.glFramebufferRenderbuffer(
GLES20.GL_FRAMEBUFFER, attachmentIndex,
GLES20.GL_DRAW_FRAMEBUFFER, attachmentIndex,
GLES20.GL_RENDERBUFFER, (int) renderTarget.colorBuffer);*/
}
else
@ -60,7 +61,7 @@ namespace Microsoft.Xna.Framework.Graphics
+ renderTarget.data2;
}
GLES20.glFramebufferTexture2D(
GLES20.GL_FRAMEBUFFER, attachmentIndex,
GLES30.GL_DRAW_FRAMEBUFFER, attachmentIndex,
attachmentType, (int) renderTarget.texture, 0);
}
attachmentIndex++;
@ -71,12 +72,12 @@ namespace Microsoft.Xna.Framework.Graphics
while (attachmentIndex < lastAttachmentPlusOne)
{
GLES20.glFramebufferRenderbuffer(
GLES20.GL_FRAMEBUFFER, attachmentIndex++,
GLES30.GL_DRAW_FRAMEBUFFER, attachmentIndex++,
GLES20.GL_RENDERBUFFER, 0);
}
GLES20.glFramebufferRenderbuffer(
GLES20.GL_FRAMEBUFFER, GLES30.GL_DEPTH_STENCIL_ATTACHMENT,
GLES30.GL_DRAW_FRAMEBUFFER, GLES30.GL_DEPTH_STENCIL_ATTACHMENT,
GLES20.GL_RENDERBUFFER, (int) depthStencilBuffer);
state.RenderToTexture = true;
@ -100,7 +101,7 @@ namespace Microsoft.Xna.Framework.Graphics
var renderer = Renderer.Get(device);
renderer.Send( () =>
{
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
GLES20.glBindFramebuffer(GLES30.GL_DRAW_FRAMEBUFFER, 0);
var state = (State) renderer.UserData;
state.RenderToTexture = false;
@ -138,7 +139,10 @@ namespace Microsoft.Xna.Framework.Graphics
GLES20.glBindTexture(attachmentType, texture);
GLES20.glGenerateMipmap(attachmentType);
GLES20.glBindTexture(attachmentType, 0);
var state = (State) renderer.UserData;
if (state.TextureOnLastUnit != 0)
GLES20.glBindTexture(attachmentType, state.TextureOnLastUnit);
});
}
}
@ -204,6 +208,75 @@ namespace Microsoft.Xna.Framework.Graphics
return ((State) Renderer.Get(graphicsDevice.GLDevice).UserData).RenderToTexture;
}
//
// Get Texture Data
//
private static void GetTextureData(Renderer renderer, int textureId,
int x, int y, int w, int h, int level,
object dataObject, int dataOffset, int dataLength)
{
java.nio.Buffer buffer = dataObject switch
{
sbyte[] byteArray =>
java.nio.ByteBuffer.wrap(byteArray, dataOffset, dataLength),
int[] intArray =>
java.nio.IntBuffer.wrap(intArray, dataOffset / 4, dataLength / 4),
_ => throw new ArgumentException(dataObject?.GetType().ToString()),
};
renderer.Send( () =>
{
var state = (State) renderer.UserData;
if (state.SourceFramebuffer == 0)
{
var id = new int[1];
GLES20.glGenFramebuffers(1, id, 0);
if ((state.SourceFramebuffer = id[0]) == 0)
return;
}
var config = state.TextureConfigs[textureId];
if (config[1] != (int) SurfaceFormat.Color)
{
throw new NotSupportedException(
((SurfaceFormat) config[1]).ToString());
}
GLES20.glBindFramebuffer(GLES30.GL_READ_FRAMEBUFFER,
state.SourceFramebuffer);
GLES20.glFramebufferTexture2D(
GLES30.GL_READ_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
GLES20.GL_TEXTURE_2D, textureId, level);
GLES20.glReadPixels(x, y, w, h,
GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buffer);
GLES20.glBindFramebuffer(GLES30.GL_READ_FRAMEBUFFER, 0);
});
}
//
// FNA3D_GetTextureData2D
//
public static void FNA3D_GetTextureData2D(IntPtr device, IntPtr texture,
int x, int y, int w, int h, int level,
IntPtr data, int dataLength)
{
// FNA Texture2D uses GCHandle::Alloc and GCHandle::AddrOfPinnedObject.
// we use GCHandle::FromIntPtr to convert that address to an object reference.
// see also: system.runtime.interopservices.GCHandle struct in baselib.
int dataOffset = (int) data;
var dataObject = System.Runtime.InteropServices.GCHandle.FromIntPtr(data).Target;
GetTextureData(Renderer.Get(device), (int) texture,
x, y, w, h, level, dataObject, dataOffset, dataLength);
}
//
// DepthFormatToDepthStorage
//
@ -238,6 +311,7 @@ namespace Microsoft.Xna.Framework.Graphics
private partial class State
{
public bool RenderToTexture;
public int SourceFramebuffer;
public int TargetFramebuffer;
public int ActiveAttachments;
}

@ -257,7 +257,7 @@ namespace Microsoft.Xna.Framework.Graphics
{
string reason = (bitmap == null) ? "unspecified error"
: $"unsupported config '{bitmap.getConfig()}'";
throw new System.BadImageFormatException(
throw new BadImageFormatException(
$"Load failed for bitmap image '{titleStream.Name}': {reason}");
}
@ -341,15 +341,22 @@ namespace Microsoft.Xna.Framework.Graphics
ref FNA3D_SamplerState sampler)
{
var samplerCopy = sampler;
int textureId = (int) texture;
var renderer = Renderer.Get(device);
renderer.Send( () =>
{
var state = (State) renderer.UserData;
var config = state.TextureConfigs[(int) texture];
var config = state.TextureConfigs[textureId];
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + index);
GLES20.glBindTexture(config[0], (int) texture);
GLES20.glBindTexture(config[0], textureId);
if (index == renderer.TextureUnits - 1)
state.TextureOnLastUnit = textureId;
if (textureId == 0)
return;
GLES20.glTexParameteri(config[0], GLES30.GL_TEXTURE_MAX_LEVEL,
config[2] - 1);
@ -571,6 +578,8 @@ namespace Microsoft.Xna.Framework.Graphics
// #1 - SurfaceFormat
// #2 - levels count
public Dictionary<int, int[]> TextureConfigs = new Dictionary<int, int[]>();
public int TextureOnLastUnit;
}
}

@ -226,3 +226,24 @@ namespace Microsoft.Xna.Framework.Input.Touch
}
}
namespace Microsoft.Xna.Framework.Media
{
//
// MediaQueue
//
[java.attr.Discard] // discard in output
public class MediaQueue
{
public MediaQueue() { }
public Song ActiveSong { get; }
public int ActiveSongIndex { get; set; }
public void Add(Song song) { }
public void Clear() { }
}
}

331
BNA/src/MediaPlayer.cs Normal file

@ -0,0 +1,331 @@
using System;
using System.Diagnostics;
#pragma warning disable 0436
namespace Microsoft.Xna.Framework.Media
{
public static class MediaPlayer
{
[java.attr.RetainType] private static android.media.MediaPlayer player;
[java.attr.RetainType] private static MediaQueue queue;
[java.attr.RetainType] private static java.util.concurrent.atomic.AtomicInteger state;
[java.attr.RetainType] private static float volume;
[java.attr.RetainType] private static bool muted;
[java.attr.RetainType] private static bool looping;
//
// static constructor
//
static MediaPlayer()
{
volume = 1f;
state = new java.util.concurrent.atomic.AtomicInteger(0);
queue = new MediaQueue();
var watcher = new Watcher();
player = new android.media.MediaPlayer();
player.setOnPreparedListener(watcher);
player.setOnCompletionListener(watcher);
}
//
// PrepareAndStart
//
private static void PrepareAndStart()
{
int oldState = state.getAndSet((int) MediaState.Playing);
player.prepareAsync();
if (oldState != (int) MediaState.Playing && MediaStateChanged != null)
{
MediaStateChanged(null, EventArgs.Empty);
}
}
//
// Play
//
public static void Play(Song song)
{
if (ActiveSongChanged != null)
throw new PlatformNotSupportedException("ActiveSongChanged");
if (song.isAsset)
{
var asset = GameRunner.Singleton.Activity.getAssets().openFd(song.path);
player.setDataSource(asset.getFileDescriptor(),
asset.getStartOffset(), asset.getLength());
}
else
player.setDataSource(song.path);
Queue.Clear();
Queue.Add(song);
Queue.ActiveSongIndex = 0;
PrepareAndStart();
}
//
// Play (SongCollection)
//
public static void Play(SongCollection songs, int index)
{
if (songs.Count == 0)
{
Queue.Clear();
Stop();
}
else if (songs.Count == 1)
{
Play((Song) (object) songs[0]);
}
else
throw new PlatformNotSupportedException();
}
public static void Play(SongCollection songs) => Play(songs, 0);
//
// Pause
//
public static void Pause()
{
if (State == MediaState.Playing)
{
if (player.isPlaying())
player.pause();
State = MediaState.Paused;
}
}
//
// Resume
//
public static void Resume()
{
if (State == MediaState.Paused)
{
player.start();
State = MediaState.Playing;
}
}
//
// Stop
//
public static void Stop()
{
if (State != MediaState.Stopped)
{
player.stop();
State = MediaState.Stopped;
}
}
//
// MoveNext, MovePrevious
//
public static void MoveNext()
{
Stop();
if (looping)
PrepareAndStart();
}
public static void MovePrevious() => MoveNext();
//
// IsMuted, Volume
//
public static bool IsMuted
{
get => muted;
set
{
muted = value;
Volume = volume;
}
}
public static float Volume
{
get => volume;
set
{
if (value < 0f)
value = 0f;
else if (value > 1f)
value = 1f;
volume = value;
if (muted)
value = 0f;
player.setVolume(value, value);
}
}
public static bool GameHasControl => true;
public static bool IsShuffled { get; set; }
//
// IsRepeating
//
public static bool IsRepeating
{
get => looping;
set
{
if (value != looping)
{
looping = value;
player.setLooping(value);
}
}
}
//
// State
//
public static MediaState State
{
get => (MediaState) state.get();
set
{
if ( state.getAndSet((int) value) != (int) value
&& MediaStateChanged != null)
{
MediaStateChanged(null, EventArgs.Empty);
}
}
}
//
// PlayPosition
//
public static TimeSpan PlayPosition
=> TimeSpan.FromMilliseconds(player.getCurrentPosition());
//
// Properties
//
public static event EventHandler<EventArgs> ActiveSongChanged;
public static event EventHandler<EventArgs> MediaStateChanged;
public static MediaQueue Queue => queue;
//
// IsVisualizationEnabled
//
public static bool IsVisualizationEnabled
{
get => false;
set => throw new PlatformNotSupportedException();
}
//
// Watcher
//
private class Watcher : android.media.MediaPlayer.OnPreparedListener,
android.media.MediaPlayer.OnCompletionListener
{
//
// onPrepared
//
[java.attr.RetainName]
public void onPrepared(android.media.MediaPlayer player)
{
if (MediaPlayer.State == MediaState.Playing)
{
try
{
var duration = player.getDuration();
if (duration != -1)
{
MediaPlayer.Queue.ActiveSong.Duration =
TimeSpan.FromMilliseconds(duration);
}
}
catch (Exception)
{
}
player.start();
}
}
//
// onCompletion
//
[java.attr.RetainName]
public void onCompletion(android.media.MediaPlayer player)
{
MediaPlayer.Stop();
}
}
}
//
// Song
//
public sealed class Song : IEquatable<Song>, IDisposable
{
[java.attr.RetainType] public string path;
[java.attr.RetainType] public bool isAsset;
public bool IsDisposed { get; private set; }
public string Name { get; private set; }
public TimeSpan Duration { get; set; }
public bool IsProtected => false;
public bool IsRated => false;
public int PlayCount => 0;
public int Rating => 0;
public int TrackNumber => 0;
public static Song FromUri(string name, Uri uri)
{
var song = new Song() { Name = name };
if (uri.IsAbsoluteUri && uri.IsFile)
song.path = uri.LocalPath;
else
{
song.path = uri.ToString();
song.isAsset = true;
}
return song;
}
~Song() => Dispose();
public void Dispose() => IsDisposed = true;
public override int GetHashCode() => base.GetHashCode();
public bool Equals(Song other) => (((object) other) != null) && (path == other.path);
public override bool Equals(object other) => Equals(other as Song);
public static bool operator ==(Song song1, Song song2)
=> (song1 == null) ? (song2 == null) : song1.Equals(song2);
public static bool operator !=(Song song1, Song song2) => ! (song1 == song2);
}
}

631
BNA/src/SoundEffect.cs Normal file

@ -0,0 +1,631 @@
using System;
using System.IO;
#pragma warning disable 0436
namespace Microsoft.Xna.Framework.Audio
{
public sealed class SoundEffect : IDisposable
{
[java.attr.RetainType] public object dataArray;
[java.attr.RetainType] public int dataCount;
[java.attr.RetainType] public int sampleRate;
[java.attr.RetainType] public int channelConfig;
[java.attr.RetainType] public int markerFrame;
[java.attr.RetainType] public static java.util.ArrayList instancesList = new java.util.ArrayList();
[java.attr.RetainType] public static java.util.concurrent.locks.ReentrantLock instancesLock = new java.util.concurrent.locks.ReentrantLock();
//
// Constructor (for ContentReader)
//
public SoundEffect(string name, byte[] buffer, int offset, int count,
ushort wFormatTag, ushort nChannels,
uint nSamplesPerSec, uint nAvgBytesPerSec,
ushort nBlockAlign, ushort wBitsPerSample,
int loopStart, int loopLength)
{
if (wFormatTag != 1 /* WAVE_FORMAT_PCM */)
throw new ArgumentException("bad wFormatTag");
if (offset != 0)
throw new ArgumentException("bad offset");
if (nBlockAlign != nChannels * wBitsPerSample / 8)
throw new ArgumentException("bad nBlockAlign");
if (nAvgBytesPerSec != nSamplesPerSec * nBlockAlign)
throw new ArgumentException("bad nAvgBytesPerSec");
sampleRate = (int) nSamplesPerSec;
channelConfig = (nChannels == 1) ? android.media.AudioFormat.CHANNEL_OUT_MONO
: (nChannels == 2) ? android.media.AudioFormat.CHANNEL_OUT_STEREO
: throw new ArgumentException("bad nChannels");
if (wBitsPerSample == 8)
{
dataArray = buffer;
dataCount = count;
}
else if (wBitsPerSample == 16)
{
int shortCount = count / 2;
var shortBuffer = new short[shortCount];
java.nio.ByteBuffer.wrap((sbyte[]) (object) buffer)
.order(java.nio.ByteOrder.LITTLE_ENDIAN).asShortBuffer()
.get(shortBuffer);
dataArray = shortBuffer;
dataCount = shortCount;
}
else
throw new ArgumentException("bad wBitsPerSample");
markerFrame = dataCount / nChannels;
Name = name;
Duration = TimeSpan.FromSeconds(count / (double) nAvgBytesPerSec);
}
//
// Constructor
//
public SoundEffect(byte[] buffer, int sampleRate, AudioChannels channels)
: this(null, buffer, 0, buffer.Length,
1 /* WAVE_FORMAT_PCM */, (ushort) channels, (uint) sampleRate,
(uint) (sampleRate * ((ushort) channels * 2)),
(ushort) ((ushort) channels * 2), 16, 0, 0)
{
}
//
// Constructor
//
public SoundEffect(byte[] buffer, int offset, int count, int sampleRate,
AudioChannels channels, int loopStart, int loopLength)
: this(null, buffer, offset, count,
1 /* WAVE_FORMAT_PCM */, (ushort) channels, (uint) sampleRate,
(uint) (sampleRate * ((ushort) channels * 2)),
(ushort) ((ushort) channels * 2), 16, loopStart, loopLength)
{
}
//
// FromStream
//
public static SoundEffect FromStream(Stream stream)
{
using (BinaryReader reader = new BinaryReader(stream))
{
for (;;)
{
if (new string(reader.ReadChars(4)) != "RIFF")
break;
reader.ReadUInt32(); // skip chunk size
if (new string(reader.ReadChars(4)) != "WAVE")
break;
if (new string(reader.ReadChars(4)) != "fmt ")
break;
if (reader.ReadInt32() != 16) // fmt chunk size always 16
break;
var wFormatTag = reader.ReadUInt16();
var nChannels = reader.ReadUInt16();
var nSamplesPerSec = reader.ReadUInt32();
var nAvgBytesPerSec = reader.ReadUInt32();
var nBlockAlign = reader.ReadUInt16();
var wBitsPerSample = reader.ReadUInt16();
if (new string(reader.ReadChars(4)) != "data")
break;
var count = reader.ReadInt32();
var buffer = reader.ReadBytes(count);
return new SoundEffect(null, buffer, 0, count, wFormatTag, nChannels,
nSamplesPerSec, nAvgBytesPerSec,
nBlockAlign, wBitsPerSample, 0, 0);
}
}
throw new BadImageFormatException("invalid wave data for sound effect");
}
//
// Destructor
//
~SoundEffect() => Dispose();
//
// Dispose
//
public void Dispose()
{
if (! IsDisposed)
{
IsDisposed = true;
DiscardInstance(null, this);
}
}
//
// Properties
//
public string Name { get; set; }
public TimeSpan Duration { get; private set; }
public bool IsDisposed { get; private set; }
//
// Play
//
public bool Play() => Play(1f, 0f, 0f);
public bool Play(float volume, float pitch, float pan)
=> CreateInstance().Play(volume, pitch, pan);
//
// CreateInstance
//
public SoundEffectInstance CreateInstance()
{
var inst = new SoundEffectInstance(this);
try
{
instancesLock.@lock();
instancesList.add(new java.lang.@ref.WeakReference(inst));
}
finally
{
instancesLock.unlock();
}
return inst;
}
//
// DiscardInstance
//
public static void DiscardInstance(SoundEffectInstance discardInstance, SoundEffect discardEffect)
{
if (instancesLock.isHeldByCurrentThread())
return;
try
{
instancesLock.@lock();
for (int idx = instancesList.size(); idx-- > 0;)
{
var instRef = (java.lang.@ref.WeakReference) instancesList.get(idx);
var inst = (SoundEffectInstance) instRef.get();
if (inst == discardInstance || inst == null || inst.ShouldDiscard(discardEffect))
instancesList.remove(idx);
}
}
finally
{
instancesLock.unlock();
}
}
//
// ReleaseInstance
//
public static bool ReleaseInstance()
{
bool found = false;
try
{
instancesLock.@lock();
int num = instancesList.size();
for (int idx = 0; idx < num; idx++)
{
var instRef = (java.lang.@ref.WeakReference) instancesList.get(idx);
var inst = (SoundEffectInstance) instRef.get();
if (inst != null && inst.ReleaseTrack(false))
{
found = true;
break;
}
}
}
finally
{
instancesLock.unlock();
}
return found;
}
//
// GetSampleDuration, GetSampleSizeInBytes
//
public static TimeSpan GetSampleDuration(int sizeInBytes, int sampleRate,
AudioChannels channels)
=> TimeSpan.FromSeconds(
((sizeInBytes / (2 * (int) channels)) / (float) sampleRate));
public static int GetSampleSizeInBytes(TimeSpan duration, int sampleRate,
AudioChannels channels)
=> (int) (duration.TotalSeconds * sampleRate * 2 * (int) channels);
//
// MasterVolume, DistanceScale, DopplerScale, SpeedOfSound (no-op)
//
public static float MasterVolume { get; set; }
public static float DistanceScale { get; set; }
public static float DopplerScale { get; set; }
public static float SpeedOfSound { get; set; }
}
//
// SoundEffectInstance
//
public class SoundEffectInstance : IDisposable
{
[java.attr.RetainType] private SoundEffect effect;
[java.attr.RetainType] private SoundEffectInstanceWatcher watcher;
[java.attr.RetainType] private android.media.AudioTrack track;
[java.attr.RetainType] private float pitch, pan, volume;
[java.attr.RetainType] private bool isLooped;
//
// Constructor (for SoundEffect.CreateInstance)
//
public SoundEffectInstance(SoundEffect fromEffect)
{
effect = fromEffect;
volume = 1f;
}
//
// CreateTrack
//
private void CreateTrack(bool tryRelease)
{
int numToWrite, numWritten;
if (effect.dataArray is sbyte[] byteData)
{
numToWrite = byteData.Length;
track = new android.media.AudioTrack(
android.media.AudioManager.STREAM_MUSIC,
effect.sampleRate, effect.channelConfig,
android.media.AudioFormat.ENCODING_PCM_8BIT,
numToWrite, android.media.AudioTrack.MODE_STATIC);
numWritten = track.write(byteData, 0, numToWrite);
}
else if (effect.dataArray is short[] shortData)
{
numToWrite = shortData.Length;
track = new android.media.AudioTrack(
android.media.AudioManager.STREAM_MUSIC,
effect.sampleRate, effect.channelConfig,
android.media.AudioFormat.ENCODING_PCM_16BIT,
numToWrite * 2, android.media.AudioTrack.MODE_STATIC);
numWritten = track.write(shortData, 0, numToWrite);
}
else
{
numToWrite = 0;
numWritten = android.media.AudioTrack.ERROR_INVALID_OPERATION;
}
if (numWritten != numToWrite)
{
track = null;
if (numWritten < 0)
{
if (SoundEffect.ReleaseInstance())
CreateTrack(false);
}
if (track == null)
{
GameRunner.Log($"SoundEffectInstance '{effect.Name}' error {numWritten}/{numToWrite}");
}
return;
}
track.setNotificationMarkerPosition(effect.markerFrame);
track.setPlaybackPositionUpdateListener(watcher = new SoundEffectInstanceWatcher());
}
//
// ReleaseTrack
//
public bool ReleaseTrack(bool disposing)
{
var track = this.track;
if (track != null)
{
if (disposing || track.getPlayState() == 1 /* android.media.AudioTrack.PLAYSTATE_STOPPED */)
{
this.track = null;
track.setPlaybackPositionUpdateListener(null);
track.stop();
track.release();
return true;
}
}
return false;
}
//
// Destructor
//
~SoundEffectInstance() => Dispose(true);
//
// Dispose
//
protected virtual void Dispose(bool disposing)
{
if (! IsDisposed)
{
ReleaseTrack(true);
SoundEffect.DiscardInstance(this, null);
effect = null;
IsDisposed = true;
}
}
public void Dispose() => Dispose(true);
//
// ShouldDiscard
//
public bool ShouldDiscard(SoundEffect fromEffect)
{
if (effect == fromEffect)
Dispose(true);
return IsDisposed;
}
//
// Properties
//
public SoundState State
{
get
{
var track = this.track;
if (track == null)
return SoundState.Stopped;
return track.getPlayState() switch
{
1 /* android.media.AudioTrack.PLAYSTATE_STOPPED */ => SoundState.Stopped,
2 /* android.media.AudioTrack.PLAYSTATE_PAUSED */ => SoundState.Paused,
3 /* android.media.AudioTrack.PLAYSTATE_PLAYING */ => SoundState.Playing,
_ => throw new InvalidOperationException()
};
}
}
public bool IsDisposed { get; protected set; }
public float Pitch
{
get => pitch;
set => SetPlaybackRate(value, State == SoundState.Playing);
}
public float Pan
{
get => pan;
set => SetStereoVolume(volume, value, State == SoundState.Playing);
}
public float Volume
{
get => volume;
set => SetStereoVolume(value, pan, State == SoundState.Playing);
}
//
// IsLooped
//
// note that looping does not respect any custom loop points,
// and always occurs on the entire effect
//
public virtual bool IsLooped
{
get => isLooped;
set
{
if (State == SoundState.Playing)
throw new InvalidOperationException();
isLooped = value;
}
}
//
// SetPlaybackRate
//
private void SetPlaybackRate(float pitch, bool playing)
{
if (pitch < -1f)
pitch = -1f;
else if (pitch > 1f)
pitch = 1f;
this.pitch = pitch;
if (playing)
{
// convert pitch from range 0 .. 1 to range 0.5 .. 2
// (which represents half to twice the sample rate)
if (pitch < 0f)
pitch = 1f + pitch * 0.5f;
else if (pitch > 0f)
pitch = 1f + pitch;
int r = (int) (effect.sampleRate * pitch);
var track = this.track;
if (track != null)
track.setPlaybackRate(r);
}
}
//
// SetStereoVolume
//
private void SetStereoVolume(float volume, float pan, bool playing)
{
if (volume < 0f)
volume = 0f;
else if (volume > 1f)
volume = 1f;
this.volume = volume;
if (pan < -1f)
pan = -1f;
else if (pan > 1f)
pan = 1f;
this.pan = pan;
if (playing)
{
float leftGain = 1f;
float rightGain = 1f;
if (pan < 0f)
{
leftGain *= 0f - pan;
rightGain *= pan + 1f;
}
else if (pan > 0f)
{
rightGain *= pan;
leftGain *= 1f - pan;
}
var track = this.track;
if (track != null)
track.setStereoVolume(leftGain * volume, rightGain * volume);
}
}
//
// Play, Pause, Resume, Stop
//
public virtual void Play()
{
if (State != SoundState.Playing)
{
var track = this.track;
if (track == null)
{
CreateTrack(true);
track = this.track;
}
if (track != null)
{
SetPlaybackRate(pitch, true);
SetStereoVolume(volume, pan, true);
watcher.instance = this;
track.play();
}
}
}
public void Pause()
{
var track = this.track;
if (track != null && State == SoundState.Playing)
track.pause();
}
public void Resume() => Play();
public void Stop() => Stop(true);
public void Stop(bool immediate)
{
if (immediate)
{
var track = this.track;
if (track != null && State != SoundState.Stopped)
track.stop();
}
if (watcher != null)
watcher.instance = null;
}
//
// Play (for SoundEffect.Play)
//
public bool Play(float volume, float pitch, float pan)
{
this.volume = volume;
this.pitch = pitch;
this.pan = pan;
Play();
if (track == null)
{
Dispose(true);
return false;
}
return true;
}
//
// Apply3D (no-op)
//
public void Apply3D(AudioListener listener, AudioEmitter emitter) { }
public void Apply3D(AudioListener[] listeners, AudioEmitter emitter) { }
}
//
// SoundEffectInstanceWatcher
//
public class SoundEffectInstanceWatcher :
android.media.AudioTrack.OnPlaybackPositionUpdateListener
{
[java.attr.RetainType] public SoundEffectInstance instance;
[java.attr.RetainName]
public void onMarkerReached(android.media.AudioTrack track)
{
// release the strong reference to the SoundEffectInstance,
// so it can be garbage collected if not otherwise referenced
track.stop();
var instance = this.instance;
if (instance != null)
{
if (instance.IsLooped && (! instance.IsDisposed))
track.play();
else
this.instance = null;
}
}
[java.attr.RetainName]
public void onPeriodicNotification(android.media.AudioTrack track) { }
}
}

@ -59,4 +59,10 @@ namespace Microsoft.Xna.Framework
}
/*internal static class TitleLocation
{
// there is no file path for asset files which are part of the APK
public static string Path => throw new System.PlatformNotSupportedException();
}*/
}

@ -60,6 +60,7 @@
<Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" />
<Reference Include="Microsoft.Xna.Framework.Input.Touch, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=MSIL" />
<Reference Include="Microsoft.Xna.Framework.Storage, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=MSIL" />
<Reference Include="System" />
<Reference Include="System.Windows.Forms" />
</ItemGroup>
<ItemGroup>

@ -3,6 +3,7 @@ using System;
using System.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Audio;
namespace Demo1
{
@ -14,6 +15,8 @@ namespace Demo1
public Texture2D white;
private SpriteBatch spriteBatch;
private Font myFont;
private SoundEffectInstance effect;
private bool playEffect;
private int pageNumber, pageNumberOld;
private bool paused;
private bool anyDrawText;
@ -58,8 +61,33 @@ namespace Demo1
// Texture2D.FromStream(GraphicsDevice, stream);
//
// to disable processing, open Properties on the image in the
// Content project, set "Build Action: None", and
// "Copy to Output Directory: Copy if newer".
// Content project, and change the following in the Advanced tab:
// "Build Action: None" and "Copy to Output Directory: Copy if newer".
effect = Content.Load<SoundEffect>("effect").CreateInstance();
effect.Pitch = 0.2f;
// the XNA content processor for Song converts music files to WMA
// format, which Android does not support playing. use MP3 instead.
//
// to disable processing, open Properties on the image in the
// Content project, and change the following in the Advanced tab:
// "Build Action: None" and "Copy to Output Directory: Copy if newer".
//
// note that XNA on Windows may not play MP3 files which contain ID3
// tags. use one of the many free utilities to remove such tags.
//
// unsupported BNA MediaPlayer: queueing more than one song at a time;
// the ActiveSongChanged event; visualization data.
Microsoft.Xna.Framework.Media.MediaPlayer.Volume = 0.05f;
Microsoft.Xna.Framework.Media.MediaPlayer.Play(
Microsoft.Xna.Framework.Media.Song.FromUri("Song1",
new System.Uri(Content.RootDirectory + "/music.mp3", UriKind.Relative)));
Microsoft.Xna.Framework.Media.MediaPlayer.IsRepeating = true;
// song duration is populated during the call to Play()
// Console.WriteLine(Microsoft.Xna.Framework.Media.MediaPlayer.Queue.ActiveSong.Duration);
}
@ -80,12 +108,26 @@ namespace Demo1
if (paused)
return;
if (playEffect)
{
effect.Play();
playEffect = false;
}
// basic scene management for the purpose of this demo:
// after either arrow at the top of the screen is clicked, and
// the current page number has changed, create the new 'page'.
if (pageNumber != pageNumberOld)
if (pageNumber != pageNumberOld)
{
if (pageComponent != null)
{
if (effect.State == SoundState.Playing)
effect.Stop();
effect.Pan = (pageNumber > pageNumberOld) ? 1f : -1f;
playEffect = true;
}
const int LAST_PAGE = 4;
if (pageNumber <= 0)
pageNumber = LAST_PAGE;

@ -42,7 +42,7 @@ namespace Demo1
{
if (renderToTexture)
{
((Game1)Game).DrawFlushBatch();
((Game1) Game).DrawFlushBatch();
renderTargetWidth = Config.ClientWidth;
renderTargetHeight = Config.ClientHeight;
if (renderTarget != null)

@ -48,6 +48,10 @@ namespace Demo1
file = container.OpenFile("state-v1.bin", FileMode.OpenOrCreate);
if (file.Length > 4)
Read(file);
// reset file in case we crash
file.SetLength(0);
file.Flush();
}
catch (Exception e)
{

@ -1,4 +1,11 @@
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!--
to resolve error in Microsoft.Xna.GameStudio.ContentPipeline.targets line 78,
error loading pipeline assembly Microsoft.Build.Framework.dll:
cd C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin
gacutil /i Microsoft.Build.Framework.dll
see also https://github.com/dotnet/msbuild/issues/1831
-->
<PropertyGroup>
<ProjectGuid>{E50B6FE1-C91C-4226-BA4B-75DEFDEF4CA3}</ProjectGuid>
<ProjectTypeGuids>{96E2B04D-8817-42c6-938A-82C39BA4D311};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
@ -64,6 +71,21 @@
<Processor>TextureProcessor</Processor>
</Compile>
</ItemGroup>
<ItemGroup>
<Compile Include="effect.mp3">
<Name>effect</Name>
<Importer>Mp3Importer</Importer>
<Processor>SoundEffectProcessor</Processor>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="music.mp3">
<Name>music</Name>
<Importer>Mp3Importer</Importer>
<Processor>SongProcessor</Processor>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Microsoft\XNA Game Studio\$(XnaFrameworkVersion)\Microsoft.Xna.GameStudio.ContentPipeline.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.

Binary file not shown.

Binary file not shown.

@ -1,5 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<!--
to resolve error in Microsoft.PackageDependencyResolution.targets about
Assets file 'project.assets.json' doesn't have a target for 'net461'
make sure you use the latest nuget.exe, or at least version 5.8.0
-->
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net461</TargetFramework>
@ -14,6 +20,9 @@
<Compile Include="Library1.fs" />
<PackageReference Include="FSharp.Core" Version="4.7.0" />
<PackageReference Update="FSharp.Core" Version="4.7.0" />
<Reference Include="Microsoft.Xna.Framework">
<HintPath>C:\Program Files (x86)\Microsoft XNA\XNA Game Studio\v4.0\References\Windows\x86\Microsoft.Xna.Framework.dll</HintPath>
</Reference>

@ -7,8 +7,10 @@ open Microsoft.Xna.Framework.Graphics
type StencilDemo (game : Game) =
inherit DrawableGameComponent(game)
let mutable spriteBatch = null
let mutable texture = null
let mutable spriteBatch = new SpriteBatch (game.GraphicsDevice)
let mutable logoTexture = game.Content.Load("fsharp256")
let mutable backTexture = new Texture2D(game.GraphicsDevice, 16, 16)
let mutable counter = 0
// using option here solely to force a dependency on FSharp.Core.dll
let mutable rectFunc : (Game -> Rectangle) option = None
@ -26,12 +28,36 @@ type StencilDemo (game : Game) =
(float32) (Math.Cos(gameTime.TotalGameTime.TotalMilliseconds * 0.001)))
override Game.Initialize() =
spriteBatch <- new SpriteBatch (game.GraphicsDevice)
texture <- game.Content.Load("fsharp256")
rectFunc <- Some logoRect
colorFunc <- Some logoColor
override Game.Draw gameTime =
let backArray = Array.zeroCreate (16 * 16)
backTexture.GetData backArray
if counter = 0
then
backArray.[12 * 16] <- 0xFF0000FF
counter <- 1
else
for y = 4 to 11 do
for x = 0 to 15 do
let idx = y * 16 + x
backArray.[idx] <- backArray.[idx + 1]
backArray.[idx + 1] <- 0
counter <- match counter with
| 60 -> 0
| n -> n + 1
backTexture.SetData backArray
spriteBatch.Begin (SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, DepthStencilState.None, new RasterizerState())
spriteBatch.Draw (backTexture,
Rectangle(0, 0, game.Window.ClientBounds.Width, game.Window.ClientBounds.Height),
Nullable (Rectangle(0, 0, 16, 16)),
Color.White)
spriteBatch.End ()
spriteBatch.Begin ()
spriteBatch.Draw (texture, rectFunc.Value game, colorFunc.Value gameTime)
spriteBatch.Draw (logoTexture, rectFunc.Value game, colorFunc.Value gameTime)
spriteBatch.End ()

@ -17,6 +17,8 @@
CONTENT_DIR = path to Content directory
e.g. $(OutputDir)/MyGame/Debug/Content
ICON_PNG = path to a PNG file to use as the app icon
APK_OUTPUT = path to copy the final APK
e.g. $(OutputDir)/MyGame.apk

@ -30,6 +30,7 @@ echo Building BNA. Command:
echo MSBuild BNA -p:Configuration=Release
echo ========================================
MSBuild BNA -p:Configuration=Release
if errorlevel 1 goto :EOF
pause
echo ========================================
@ -38,14 +39,17 @@ echo nuget restore Demo1
echo msbuild Demo1 -p:Configuration=Release -p:Platform="x86"
echo ========================================
nuget restore Demo1
if errorlevel 1 goto :EOF
msbuild Demo1 -p:Configuration=Release -p:Platform="x86"
if errorlevel 1 goto :EOF
pause
echo ========================================
echo Converting Demo1 to APK. Command:
echo MSBuild MakeAPK.project -p:INPUT_DLL=.obj\Demo1\Release\Demo1.exe -p:INPUT_DLL_2=.obj\Demo1\Release\Demo1FSharp.dll -p:INPUT_DLL_3=.obj\Demo1\Release\FSharp.Core.dll -p:CONTENT_DIR=.obj\Demo1\Release\Content -p:ICON_PNG=Demo1\Demo1\GameThumbnail.png -p:ANDROID_MANIFEST=Demo1\AndroidManifest.xml -p:KEYSTORE_FILE=.\my.keystore -p:KEYSTORE_PWD=123456 -p:APK_OUTPUT=.obj\Demo1.apk -p:APK_TEMP_DIR=.obj\Demo1\Release\TempApk -p:EXTRA_JAR_1=.obj\BNA.jar -p:EXTRA_JAR_2=%BLUEBONNET_LIB%
echo ========================================
MSBuild MakeAPK.project -p:INPUT_DLL=.obj\Demo1\Release\Demo1.exe -p:INPUT_DLL_2=.obj\Demo1\Release\Demo1FSharp.dll -p:INPUT_DLL_3=.obj\Demo1\Release\FSharp.Core.dll -p:CONTENT_DIR=.obj\Demo1\Release\Content -p:ICON_PNG=Demo1\Demo1\GameThumbnail.png -p:ANDROID_MANIFEST=Demo1\AndroidManifest.xml -p:KEYSTORE_FILE=.\my.keystore -p:KEYSTORE_PWD=123456 -p:APK_OUTPUT=.obj\Demo1.apk -p:APK_TEMP_DIR=.obj\Demo1\Release\TempApk -p:EXTRA_JAR_1=.obj\BNA.jar -p:EXTRA_JAR_2=%BLUEBONNET_LIB%
MSBuild MakeAPK.project -p:INPUT_DLL=.obj\Demo1\Release\Demo1.exe -p:INPUT_DLL_2=.obj\Demo1\Release\Demo1FSharp.dll -p:INPUT_DLL_3=.obj\Demo1\Release\FSharp.Core.dll -p:CONTENT_DIR=.obj\Demo1\Release\Content -p:ICON_PNG=Demo1\Demo1\GameThumbnail.png -p:ANDROID_MANIFEST=Demo1\AndroidManifest.xml -p:KEYSTORE_FILE=.\my.keystore -p:KEYSTORE_PWD=123456 -p:APK_OUTPUT=.obj\Demo1.apk -p:APK_TEMP_DIR=.obj\Demo1\Release\TempApk -p:EXTRA_JAR_1=.obj\BNA.jar -p:EXTRA_JAR_2=%BLUEBONNET_LIB%
if errorlevel 1 goto :EOF
echo ========================================
echo All done