Fix errors when parsing audio
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
15
.metadata
15
.metadata
@@ -15,24 +15,9 @@ migration:
|
||||
- platform: root
|
||||
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||
- platform: android
|
||||
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||
- platform: ios
|
||||
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||
- platform: linux
|
||||
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||
- platform: macos
|
||||
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||
- platform: web
|
||||
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||
- platform: windows
|
||||
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||
|
||||
# User provided section
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_soloud/flutter_soloud.dart';
|
||||
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_synth/wolf_3d_synth.dart';
|
||||
import 'package:wolf_dart/features/difficulty/difficulty.dart';
|
||||
import 'package:wolf_dart/features/renderer/renderer.dart';
|
||||
|
||||
class DifficultyScreen extends StatelessWidget {
|
||||
class DifficultyScreen extends StatefulWidget {
|
||||
const DifficultyScreen(
|
||||
this.data, {
|
||||
super.key,
|
||||
@@ -11,7 +13,86 @@ class DifficultyScreen extends StatelessWidget {
|
||||
|
||||
final WolfensteinData data;
|
||||
|
||||
bool get isShareware => data.version == GameVersion.shareware;
|
||||
@override
|
||||
State<DifficultyScreen> createState() => _DifficultyScreenState();
|
||||
}
|
||||
|
||||
class _DifficultyScreenState extends State<DifficultyScreen> {
|
||||
AudioSource? _menuMusicSource;
|
||||
SoundHandle? _menuMusicHandle;
|
||||
|
||||
bool get isShareware => widget.data.version == GameVersion.shareware;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_playMenuMusic();
|
||||
}
|
||||
|
||||
Future<void> _playMenuMusic() async {
|
||||
final soloud = SoLoud.instance;
|
||||
|
||||
if (!soloud.isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. We only want to play music if the IMF data actually exists
|
||||
if (widget.data.music.isNotEmpty) {
|
||||
// Get the first track (usually the menu theme "Wondering About My Loved Ones")
|
||||
final music = widget.data.music.first;
|
||||
|
||||
// Render the hardware instructions into PCM and wrap in a WAV header
|
||||
final pcmSamples = ImfRenderer.render(music);
|
||||
|
||||
final wavBytes = ImfRenderer.createWavFile(pcmSamples);
|
||||
|
||||
// 3. Load the bytes into SoLoud's memory
|
||||
// The 'menu_theme.wav' string is just a dummy name to tell SoLoud it's dealing with a WAV format
|
||||
_menuMusicSource = await soloud.loadMem('menu_theme.wav', wavBytes);
|
||||
|
||||
// 4. Play the source and tell it to loop continuously!
|
||||
_menuMusicHandle = await soloud.play(
|
||||
_menuMusicSource!,
|
||||
looping: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cleanupAudio();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _cleanupAudio() {
|
||||
final soloud = SoLoud.instance;
|
||||
|
||||
// Stop the playback
|
||||
if (_menuMusicHandle != null) {
|
||||
soloud.stop(_menuMusicHandle!);
|
||||
}
|
||||
|
||||
// Free the raw WAV data from C++ memory
|
||||
if (_menuMusicSource != null) {
|
||||
soloud.disposeSource(_menuMusicSource!);
|
||||
}
|
||||
}
|
||||
|
||||
void _startGame(Difficulty difficulty, {bool showGallery = false}) {
|
||||
// Stop the music and clear memory right before we push the new route
|
||||
_cleanupAudio();
|
||||
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => WolfRenderer(
|
||||
widget.data,
|
||||
difficulty: difficulty,
|
||||
isShareware: isShareware,
|
||||
showSpriteGallery: showGallery,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -19,18 +100,7 @@ class DifficultyScreen extends StatelessWidget {
|
||||
backgroundColor: Colors.black,
|
||||
floatingActionButton: FloatingActionButton(
|
||||
backgroundColor: Colors.red[900],
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => WolfRenderer(
|
||||
data,
|
||||
difficulty: Difficulty.bringEmOn,
|
||||
isShareware: isShareware,
|
||||
showSpriteGallery: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onPressed: () => _startGame(Difficulty.bringEmOn, showGallery: true),
|
||||
child: const Icon(Icons.bug_report, color: Colors.white),
|
||||
),
|
||||
body: Center(
|
||||
@@ -46,6 +116,7 @@ class DifficultyScreen extends StatelessWidget {
|
||||
fontFamily: 'Courier',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// --- Difficulty Buttons ---
|
||||
ListView.builder(
|
||||
@@ -64,17 +135,7 @@ class DifficultyScreen extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => WolfRenderer(
|
||||
data,
|
||||
difficulty: difficulty,
|
||||
isShareware: isShareware,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onPressed: () => _startGame(difficulty),
|
||||
child: Text(
|
||||
difficulty.title,
|
||||
style: const TextStyle(fontSize: 18),
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_soloud/flutter_soloud.dart';
|
||||
import 'package:wolf_dart/game_select_screen.dart';
|
||||
|
||||
void main() {
|
||||
void main() async {
|
||||
await SoLoud.instance.init(
|
||||
sampleRate: 44100, // Audio quality
|
||||
bufferSize: 2048, // Buffer size affects latency
|
||||
channels: Channels.stereo,
|
||||
);
|
||||
|
||||
runApp(
|
||||
const MaterialApp(
|
||||
home: GameSelectScreen(),
|
||||
|
||||
@@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
flutter_soloud
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
@@ -62,7 +62,7 @@ abstract class WLParser {
|
||||
}) {
|
||||
final isShareware = version == GameVersion.shareware;
|
||||
|
||||
final audio = parseAudio(audioHed, audioT);
|
||||
final audio = parseAudio(audioHed, audioT, version);
|
||||
|
||||
return WolfensteinData(
|
||||
version: version,
|
||||
@@ -295,9 +295,9 @@ abstract class WLParser {
|
||||
static ({List<AdLibSound> adLib, List<ImfMusic> music}) parseAudio(
|
||||
ByteData audioHed,
|
||||
ByteData audioT,
|
||||
GameVersion version,
|
||||
) {
|
||||
List<int> offsets = [];
|
||||
// AUDIOHED is a series of 32-bit unsigned integers
|
||||
for (int i = 0; i < audioHed.lengthInBytes ~/ 4; i++) {
|
||||
offsets.add(audioHed.getUint32(i * 4, Endian.little));
|
||||
}
|
||||
@@ -308,7 +308,6 @@ abstract class WLParser {
|
||||
int start = offsets[i];
|
||||
int next = offsets[i + 1];
|
||||
|
||||
// 0xFFFFFFFF (or 4294967295) marks an empty slot
|
||||
if (start == 0xFFFFFFFF || start >= audioT.lengthInBytes) {
|
||||
allAudioChunks.add(Uint8List(0));
|
||||
continue;
|
||||
@@ -324,16 +323,18 @@ abstract class WLParser {
|
||||
}
|
||||
}
|
||||
|
||||
// Wolfenstein 3D split:
|
||||
// Chunks 0-299: AdLib Sounds
|
||||
// Chunks 300+: IMF Music
|
||||
// --- THE FIX: Accurate Historical Chunk Indices ---
|
||||
// WL1 (Shareware) has exactly 234 sound effects before the music.
|
||||
// WL6 (Retail) has exactly 261 sound effects before the music.
|
||||
int musicStartIndex = (version == GameVersion.shareware) ? 234 : 261;
|
||||
|
||||
List<AdLibSound> adLib = allAudioChunks
|
||||
.take(300)
|
||||
.take(musicStartIndex)
|
||||
.map((bytes) => AdLibSound(bytes))
|
||||
.toList();
|
||||
|
||||
List<ImfMusic> music = allAudioChunks
|
||||
.skip(300)
|
||||
.skip(musicStartIndex)
|
||||
.where((chunk) => chunk.isNotEmpty)
|
||||
.map((bytes) => ImfMusic.fromBytes(bytes))
|
||||
.toList();
|
||||
|
||||
@@ -30,13 +30,19 @@ class ImfMusic {
|
||||
factory ImfMusic.fromBytes(Uint8List bytes) {
|
||||
List<ImfInstruction> instructions = [];
|
||||
|
||||
// Read the file in 4-byte chunks
|
||||
for (int i = 0; i < bytes.length - 3; i += 4) {
|
||||
// Wolfenstein 3D IMF chunks start with a 16-bit length header (little-endian)
|
||||
int actualSize = bytes[0] | (bytes[1] << 8);
|
||||
|
||||
// Start parsing at index 2 to skip the size header
|
||||
int limit = 2 + actualSize;
|
||||
if (limit > bytes.length) limit = bytes.length; // Safety bounds
|
||||
|
||||
for (int i = 2; i < limit - 3; i += 4) {
|
||||
instructions.add(
|
||||
ImfInstruction(
|
||||
register: bytes[i],
|
||||
data: bytes[i + 1],
|
||||
delay: bytes[i + 2] | (bytes[i + 3] << 8), // 16-bit little-endian
|
||||
delay: bytes[i + 2] | (bytes[i + 3] << 8),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
91
packages/wolf_3d_synth/lib/src/imf_renderer.dart
Normal file
91
packages/wolf_3d_synth/lib/src/imf_renderer.dart
Normal file
@@ -0,0 +1,91 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
|
||||
|
||||
import 'opl2_emulator.dart';
|
||||
|
||||
class ImfRenderer {
|
||||
/// Renders an ImfMusic track into raw 16-bit PCM samples
|
||||
static Int16List render(ImfMusic music) {
|
||||
final emulator = Opl2Emulator();
|
||||
|
||||
const double samplesPerTick = Opl2Emulator.sampleRate / 700.0;
|
||||
|
||||
// 1. Pre-calculate the total number of audio samples we need
|
||||
int totalTicks = 0;
|
||||
for (final inst in music.instructions) {
|
||||
totalTicks += inst.delay;
|
||||
}
|
||||
int totalSamples = (totalTicks * samplesPerTick).round();
|
||||
|
||||
// 2. Pre-allocate a fixed-size buffer (blazing fast!)
|
||||
final pcmBuffer = Int16List(totalSamples);
|
||||
int sampleIndex = 0;
|
||||
|
||||
for (final instruction in music.instructions) {
|
||||
// Write the instruction to the chip
|
||||
emulator.writeRegister(instruction.register, instruction.data);
|
||||
|
||||
// Generate the samples for the delay period
|
||||
int samplesToGenerate = (instruction.delay * samplesPerTick).round();
|
||||
|
||||
for (int i = 0; i < samplesToGenerate; i++) {
|
||||
if (sampleIndex >= pcmBuffer.length) break; // Safety bounds
|
||||
|
||||
double sampleFloat = emulator.generateSample();
|
||||
int sampleInt = (sampleFloat * 32767).toInt().clamp(-32768, 32767);
|
||||
|
||||
pcmBuffer[sampleIndex++] = sampleInt;
|
||||
}
|
||||
}
|
||||
|
||||
return pcmBuffer;
|
||||
}
|
||||
|
||||
/// Wraps raw 16-bit PCM data in a standard WAV file header
|
||||
static Uint8List createWavFile(Int16List pcmData) {
|
||||
int byteRate = Opl2Emulator.sampleRate * 2; // 1 channel * 16-bit (2 bytes)
|
||||
int dataSize = pcmData.length * 2;
|
||||
int fileSize = 36 + dataSize;
|
||||
|
||||
final bytes = BytesBuilder();
|
||||
|
||||
// "RIFF" chunk descriptor
|
||||
bytes.add('RIFF'.codeUnits);
|
||||
bytes.add(_int32ToBytes(fileSize));
|
||||
bytes.add('WAVE'.codeUnits);
|
||||
|
||||
// "fmt " sub-chunk
|
||||
bytes.add('fmt '.codeUnits);
|
||||
bytes.add(_int32ToBytes(16)); // Subchunk1Size (16 for PCM)
|
||||
bytes.add(_int16ToBytes(1)); // AudioFormat (1 = PCM)
|
||||
bytes.add(_int16ToBytes(1)); // NumChannels (1 = Mono)
|
||||
bytes.add(_int32ToBytes(Opl2Emulator.sampleRate)); // SampleRate
|
||||
bytes.add(_int32ToBytes(byteRate)); // ByteRate
|
||||
bytes.add(_int16ToBytes(2)); // BlockAlign
|
||||
bytes.add(_int16ToBytes(16)); // BitsPerSample
|
||||
|
||||
// "data" sub-chunk
|
||||
bytes.add('data'.codeUnits);
|
||||
bytes.add(_int32ToBytes(dataSize));
|
||||
|
||||
// Append the actual raw audio data
|
||||
for (int sample in pcmData) {
|
||||
bytes.add(_int16ToBytes(sample));
|
||||
}
|
||||
|
||||
return bytes.toBytes();
|
||||
}
|
||||
|
||||
static List<int> _int16ToBytes(int value) => [
|
||||
value & 0xff,
|
||||
(value >> 8) & 0xff,
|
||||
];
|
||||
|
||||
static List<int> _int32ToBytes(int value) => [
|
||||
value & 0xff,
|
||||
(value >> 8) & 0xff,
|
||||
(value >> 16) & 0xff,
|
||||
(value >> 24) & 0xff,
|
||||
];
|
||||
}
|
||||
86
packages/wolf_3d_synth/lib/src/opl2_emulator.dart
Normal file
86
packages/wolf_3d_synth/lib/src/opl2_emulator.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
class Opl2Channel {
|
||||
int fNum = 0;
|
||||
int block = 0;
|
||||
bool keyOn = false;
|
||||
|
||||
double phase = 0.0;
|
||||
double phaseIncrement = 0.0;
|
||||
|
||||
void updateFrequency() {
|
||||
// The magic Yamaha YM3812 frequency formula!
|
||||
// 49716Hz is the internal sample rate of the original 1980s silicon.
|
||||
double realFreq = (fNum * math.pow(2, block)) * (49716.0 / 1048576.0);
|
||||
|
||||
// Convert that physical frequency into a phase step for our modern 44100Hz output
|
||||
phaseIncrement = (realFreq * 2 * math.pi) / Opl2Emulator.sampleRate;
|
||||
}
|
||||
|
||||
double getSample() {
|
||||
// If the note isn't being pressed, return silence
|
||||
if (!keyOn) return 0.0;
|
||||
|
||||
// Generate the raw sine wave output
|
||||
double out = math.sin(phase);
|
||||
|
||||
// Advance the phase for the next frame
|
||||
phase += phaseIncrement;
|
||||
if (phase >= 2 * math.pi) {
|
||||
phase -= 2 * math.pi;
|
||||
}
|
||||
|
||||
// Scale the volume down significantly. If 9 channels play at 1.0 volume,
|
||||
// the audio will severely clip and distort.
|
||||
return out * 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
class Opl2Emulator {
|
||||
static const int sampleRate = 44100;
|
||||
|
||||
// The 9 independent audio channels
|
||||
final List<Opl2Channel> channels = List.generate(9, (_) => Opl2Channel());
|
||||
|
||||
void writeRegister(int reg, int data) {
|
||||
// --- 0xA0 - 0xA8: F-Number (Lower 8 bits) ---
|
||||
if (reg >= 0xA0 && reg <= 0xA8) {
|
||||
int channelIdx = reg - 0xA0;
|
||||
|
||||
// Keep the upper 2 bits intact, replace the lower 8 bits
|
||||
channels[channelIdx].fNum = (channels[channelIdx].fNum & 0x300) | data;
|
||||
channels[channelIdx].updateFrequency();
|
||||
}
|
||||
// --- 0xB0 - 0xB8: Key-On, Block, F-Number (Upper 2 bits) ---
|
||||
else if (reg >= 0xB0 && reg <= 0xB8) {
|
||||
int channelIdx = reg - 0xB0;
|
||||
Opl2Channel channel = channels[channelIdx];
|
||||
|
||||
// Extract the bits using bitwise masks
|
||||
channel.keyOn = (data & 0x20) != 0; // Bit 5
|
||||
channel.block = (data & 0x1C) >> 2; // Bits 2-4
|
||||
int fNumHigh = (data & 0x03) << 8; // Bits 0-1
|
||||
|
||||
// Keep the lower 8 bits intact, replace the upper 2 bits
|
||||
channel.fNum = (channel.fNum & 0xFF) | fNumHigh;
|
||||
channel.updateFrequency();
|
||||
|
||||
// When a new note is struck, we reset the phase counter to 0
|
||||
if (channel.keyOn) {
|
||||
channel.phase = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mixes the 9 channels down into a single PCM audio sample
|
||||
double generateSample() {
|
||||
double mixedOutput = 0.0;
|
||||
|
||||
for (var channel in channels) {
|
||||
mixedOutput += channel.getSample();
|
||||
}
|
||||
|
||||
// Hard limiter to prevent audio popping if the mix exceeds 1.0
|
||||
return mixedOutput.clamp(-1.0, 1.0);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// TODO: Put public facing types in this file.
|
||||
|
||||
/// Checks if you are awesome. Spoiler: you are.
|
||||
class Awesome {
|
||||
bool get isAwesome => true;
|
||||
}
|
||||
@@ -3,6 +3,4 @@
|
||||
/// More dartdocs go here.
|
||||
library;
|
||||
|
||||
export 'src/wolf_3d_synth_base.dart';
|
||||
|
||||
// TODO: Export any libraries intended for clients of this package.
|
||||
export 'src/imf_renderer.dart' show ImfRenderer;
|
||||
|
||||
@@ -8,6 +8,9 @@ environment:
|
||||
|
||||
resolution: workspace
|
||||
|
||||
dependencies:
|
||||
wolf_3d_data_types:
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^6.0.0
|
||||
test: ^1.25.6
|
||||
|
||||
@@ -9,8 +9,10 @@ environment:
|
||||
dependencies:
|
||||
wolf_3d_data: any
|
||||
wolf_3d_data_types: any
|
||||
wolf_3d_synth: any
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_soloud: ^3.5.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -24,4 +26,6 @@ flutter:
|
||||
- assets/shareware/
|
||||
|
||||
workspace:
|
||||
- packages/*
|
||||
- packages/wolf_3d_data/
|
||||
- packages/wolf_3d_data_types/
|
||||
- packages/wolf_3d_synth/
|
||||
|
||||
BIN
web/favicon.png
Normal file
BIN
web/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 917 B |
BIN
web/icons/Icon-192.png
Normal file
BIN
web/icons/Icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
web/icons/Icon-512.png
Normal file
BIN
web/icons/Icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
BIN
web/icons/Icon-maskable-192.png
Normal file
BIN
web/icons/Icon-maskable-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
BIN
web/icons/Icon-maskable-512.png
Normal file
BIN
web/icons/Icon-maskable-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
51
web/index.html
Normal file
51
web/index.html
Normal file
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<!--
|
||||
If you are serving your web app in a path other than the root, change the
|
||||
href value below to reflect the base path you are serving from.
|
||||
|
||||
The path provided below has to start and end with a slash "/" in order for
|
||||
it to work correctly.
|
||||
|
||||
For more details:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||
|
||||
This is a placeholder for base href that will be replaced by the value of
|
||||
the `--base-href` argument provided to `flutter build`.
|
||||
-->
|
||||
<base href="$FLUTTER_BASE_HREF">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="wolf_dart">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
|
||||
<title>wolf_dart</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!--
|
||||
You can customize the "flutter_bootstrap.js" script.
|
||||
This is useful to provide a custom configuration to the Flutter loader
|
||||
or to give the user feedback during the initialization process.
|
||||
|
||||
For more details:
|
||||
* https://docs.flutter.dev/platform-integration/web/initialization
|
||||
-->
|
||||
<script src="flutter_bootstrap.js" async></script>
|
||||
<script src="assets/packages/flutter_soloud/web/libflutter_soloud_plugin.js" defer></script>
|
||||
<script src="assets/packages/flutter_soloud/web/init_module.dart.js" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
35
web/manifest.json
Normal file
35
web/manifest.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "wolf_dart",
|
||||
"short_name": "wolf_dart",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#0175C2",
|
||||
"theme_color": "#0175C2",
|
||||
"description": "A new Flutter project.",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/Icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user